模組:沙盒/a2569875/PythonScript/Librarys/Structes
外观
-- Module:PythonScript/Librarys/Structes
-- 綜合結構標準庫:包含 struct, logging, unittest, calendar
local Structes = {}
local PyClass = require("Module:沙盒/a2569875/PythonScript/Class")
local Runtime = require("Module:沙盒/a2569875/PythonScript/Runtime")
local RUtils = require("Module:沙盒/a2569875/PythonScript/Runtime/Utils")
local EvalUtils = require("Module:沙盒/a2569875/PythonScript/Evaluator/Utils")
local Evaluator = require("Module:沙盒/a2569875/PythonScript/Evaluator")
local B = Runtime.builtins
local function fn(name, func) return { __type = "builtin_method", __name = name, __call = func } end
-- 輔助:安全將任意 Python 數值解包為 Lua 原生 number
local function to_num(x)
if type(x) == "number" then return x end
if type(x) == "boolean" then return x and 1 or 0 end
if type(x) == "table" then
if x.__type == "int" then return x.value or tonumber(RUtils.to_str(x, false)) or 0 end
if x.__builtin_value ~= nil then return to_num(x.__builtin_value) end
end
return tonumber(x) or 0
end
-- 輔助:安全檢查類別繼承關係 (支援單一 class 或 tuple of classes)
local function is_subclass(sub, target)
if type(sub) ~= "table" or type(target) ~= "table" then return false end
if target.__type == "tuple" then
for _, t in ipairs(target.elements) do
if is_subclass(sub, t) then return true end
end
return false
end
if sub == target then return true end
local mro = sub.__mro or sub.__mro__
if mro then
for _, base in ipairs(mro) do
if base == target then return true end
end
end
if sub.__bases then
for _, base in ipairs(sub.__bases) do
if is_subclass(base, target) then return true end
end
end
return false
end
-- 輔助:建立模組命名空間
local function make_namespace(name, dict)
local mod_cls = PyClass.make_class("module", {}, {})
local inst = PyClass.make_instance(mod_cls)
inst.__dict = dict
inst.__dict["__name__"] = name
for _, v in pairs(dict) do
if type(v) == "table" and v.__type == "class" then
v.__dict["__module__"] = name
end
end
return inst
end
-- ==========================================
-- 1. STRUCT 模組 (二進位資料打包與解包,支援 32/64-bit 浮點數)
-- ==========================================
function Structes.init_struct()
local struct_dict = {}
local StructError = PyClass.make_class("error", {B.Exception}, {})
struct_dict.error = StructError
local function raise_struct_err(msg)
local exc = PyClass.make_instance(StructError)
PyClass.set_attr(exc, "args", { __type = "tuple", elements = {msg} })
Runtime.raise(exc)
end
local function get_fmt_info(fmt)
if type(fmt) == "table" and fmt._data then fmt = fmt._data end
local endian = "<"
local pos = 1
local first_char = fmt:sub(1, 1)
if first_char == "@" or first_char == "=" or first_char == "<" or first_char == ">" or first_char == "!" then
endian = (first_char == ">" or first_char == "!") and ">" or "<"
pos = 2
end
local codes = {}
for count_str, code in fmt:sub(pos):gmatch("(%d*)([a-zA-Z?])") do
local count = tonumber(count_str) or 1
if code == "s" or code == "p" then
table.insert(codes, {code = code, count = count})
else
for _ = 1, count do table.insert(codes, {code = code, count = 1}) end
end
end
return endian, codes
end
local type_sizes = {x=1, c=1, b=1, B=1, ["?"]=1, h=2, H=2, i=4, I=4, l=4, L=4, q=8, Q=8, f=4, d=8}
local function calcsize(fmt)
local _, codes = get_fmt_info(fmt)
local size = 0
for _, c in ipairs(codes) do
if c.code == "s" or c.code == "p" then size = size + c.count
elseif type_sizes[c.code] then size = size + type_sizes[c.code]
else raise_struct_err("bad char in struct format") end
end
return size
end
local function pack_int(val, size, is_signed, is_big)
val = math.floor(to_num(val))
local bytes = {}
local u = val
if is_signed and u < 0 then u = u + (256 ^ size) end
for _ = 1, size do
local r = u % 256
table.insert(bytes, string.char(r))
u = math.floor(u / 256)
end
local res = table.concat(bytes)
return is_big and res:reverse() or res
end
local function unpack_int(s, pos, size, is_signed, is_big)
local chunk = s:sub(pos, pos + size - 1)
if is_big then chunk = chunk:reverse() end
local val, mult = 0, 1
for i = 1, size do
val = val + string.byte(chunk, i) * mult
mult = mult * 256
end
if is_signed then
local half = 2 ^ (size * 8 - 1)
if val >= half then val = val - 2 * half end
end
return val
end
local function pack_float32(x, is_big)
x = to_num(x)
if x == 0 then return pack_int(0, 4, false, is_big) end
local sign = 0; if x < 0 then sign = 128; x = -x end
local mant, exp = math.frexp(x); exp = exp + 126
if exp <= 0 then mant = math.floor(mant * 2 ^ (24 + exp) + 0.5); exp = 0
else mant = math.floor((mant * 2 - 1) * 2 ^ 23 + 0.5); if mant >= 2 ^ 23 then mant = 0; exp = exp + 1 end end
local b1 = mant % 256; mant = math.floor(mant / 256)
local b2 = mant % 256; local b3 = math.floor(mant / 256) + (exp % 2) * 128
local b4 = sign + math.floor(exp / 2)
local res = string.char(b1, b2, b3, b4)
return is_big and res:reverse() or res
end
local function unpack_float32(s, pos, is_big)
local chunk = s:sub(pos, pos + 3)
if is_big then chunk = chunk:reverse() end
local b1, b2, b3, b4 = string.byte(chunk, 1, 4)
local sign = (b4 >= 128) and -1 or 1; local exp = (b4 % 128) * 2 + math.floor(b3 / 128)
local mant = ((b3 % 128) * 256 + b2) * 256 + b1
if exp == 0 and mant == 0 then return 0.0 * sign
elseif exp == 255 then return (mant == 0) and (math.huge * sign) or (0/0)
elseif exp == 0 then return sign * math.ldexp(mant, -149)
else return sign * math.ldexp(mant + 2 ^ 23, exp - 150) end
end
local function pack_float64(x, is_big)
x = to_num(x)
if x == 0 then return pack_int(0, 8, false, is_big) end
local sign = 0; if x < 0 then sign = 128; x = -x end
if x == math.huge then
local res = string.char(0, 0, 0, 0, 0, 0, 240, sign + 127)
return is_big and res:reverse() or res
elseif x ~= x then
local res = string.char(0, 0, 0, 0, 0, 0, 248, sign + 127)
return is_big and res:reverse() or res
end
local mant, exp = math.frexp(x); exp = exp + 1022
if exp <= 0 then mant = math.floor(mant * 2 ^ (53 + exp) + 0.5); exp = 0
else mant = math.floor((mant * 2 - 1) * 2 ^ 52 + 0.5); if mant >= 2 ^ 52 then mant = 0; exp = exp + 1 end end
local bytes = {}
for _ = 1, 6 do table.insert(bytes, string.char(mant % 256)); mant = math.floor(mant / 256) end
local b7 = mant + (exp % 16) * 16
local b8 = sign + math.floor(exp / 16)
table.insert(bytes, string.char(b7, b8))
local res = table.concat(bytes)
return is_big and res:reverse() or res
end
local function unpack_float64(s, pos, is_big)
local chunk = s:sub(pos, pos + 7)
if is_big then chunk = chunk:reverse() end
local b1, b2, b3, b4, b5, b6, b7, b8 = string.byte(chunk, 1, 8)
local sign = (b8 >= 128) and -1 or 1
local exp = (b8 % 128) * 16 + math.floor(b7 / 16)
local mant = (b7 % 16)
for _, b in ipairs({b6, b5, b4, b3, b2, b1}) do mant = mant * 256 + b end
if exp == 0 and mant == 0 then return 0.0 * sign
elseif exp == 2047 then return (mant == 0) and (math.huge * sign) or (0/0)
elseif exp == 0 then return sign * math.ldexp(mant, -1074)
else return sign * math.ldexp(mant + 2 ^ 52, exp - 1075) end
end
local function raw_pack(fmt, args, start_idx)
local endian, codes = get_fmt_info(fmt)
local is_big = (endian == ">")
local res = {}
local arg_idx = start_idx or 2
for _, c in ipairs(codes) do
local code = c.code
if code == "x" then
table.insert(res, "\0")
elseif code == "s" or code == "p" then
local val = args[arg_idx]; arg_idx = arg_idx + 1
if type(val) == "table" and val._data then val = val._data end
val = tostring(val or "")
if #val < c.count then val = val .. string.rep("\0", c.count - #val)
elseif #val > c.count then val = val:sub(1, c.count) end
table.insert(res, val)
else
local val = args[arg_idx]; arg_idx = arg_idx + 1
if code == "?" then table.insert(res, pack_int(EvalUtils.is_truthy(val) and 1 or 0, 1, false, is_big))
elseif code == "b" then table.insert(res, pack_int(val, 1, true, is_big))
elseif code == "B" or code == "c" then
if type(val) == "string" then val = string.byte(val)
elseif type(val) == "table" and val._data then val = string.byte(val._data) end
table.insert(res, pack_int(val, 1, false, is_big))
elseif code == "h" then table.insert(res, pack_int(val, 2, true, is_big))
elseif code == "H" then table.insert(res, pack_int(val, 2, false, is_big))
elseif code == "i" or code == "l" then table.insert(res, pack_int(val, 4, true, is_big))
elseif code == "I" or code == "L" then table.insert(res, pack_int(val, 4, false, is_big))
elseif code == "q" then table.insert(res, pack_int(val, 8, true, is_big))
elseif code == "Q" then table.insert(res, pack_int(val, 8, false, is_big))
elseif code == "f" then table.insert(res, pack_float32(val, is_big))
elseif code == "d" then table.insert(res, pack_float64(val, is_big))
else raise_struct_err("bad char in struct format") end
end
end
return { __type = "bytes", _data = table.concat(res) }
end
local function raw_unpack(fmt, buffer, offset)
if type(buffer) == "table" and buffer._data then buffer = buffer._data end
local expected_size = calcsize(fmt)
local pos = (offset or 0) + 1
if #buffer - pos + 1 < expected_size then
raise_struct_err(string.format("unpack requires a buffer of %d bytes", expected_size))
end
local endian, codes = get_fmt_info(fmt)
local is_big = (endian == ">")
local elts = {}
for _, c in ipairs(codes) do
local code = c.code
if code == "x" then
pos = pos + 1
elseif code == "s" or code == "p" then
table.insert(elts, { __type = "bytes", _data = buffer:sub(pos, pos + c.count - 1) })
pos = pos + c.count
else
local val = 0
if code == "?" then val = (unpack_int(buffer, pos, 1, false, is_big) ~= 0)
elseif code == "b" then val = unpack_int(buffer, pos, 1, true, is_big)
elseif code == "B" then val = unpack_int(buffer, pos, 1, false, is_big)
elseif code == "c" then val = { __type = "bytes", _data = string.char(unpack_int(buffer, pos, 1, false, is_big)) }
elseif code == "h" then val = unpack_int(buffer, pos, 2, true, is_big)
elseif code == "H" then val = unpack_int(buffer, pos, 2, false, is_big)
elseif code == "i" or code == "l" then val = unpack_int(buffer, pos, 4, true, is_big)
elseif code == "I" or code == "L" then val = unpack_int(buffer, pos, 4, false, is_big)
elseif code == "q" then val = unpack_int(buffer, pos, 8, true, is_big)
elseif code == "Q" then val = unpack_int(buffer, pos, 8, false, is_big)
elseif code == "f" then val = unpack_float32(buffer, pos, is_big)
elseif code == "d" then val = unpack_float64(buffer, pos, is_big) end
table.insert(elts, val)
pos = pos + (type_sizes[code] or 0)
end
end
return { __type = "tuple", elements = elts }
end
struct_dict.calcsize = fn("calcsize", function(a) return calcsize(a[1]) end)
struct_dict.pack = fn("pack", function(a) return raw_pack(a[1], a, 2) end)
struct_dict.unpack = fn("unpack", function(a)
local buf = a[2]
if type(buf) == "table" and buf._data then buf = buf._data end
local sz = calcsize(a[1])
if #buf ~= sz then raise_struct_err(string.format("unpack requires a buffer of %d bytes", sz)) end
return raw_unpack(a[1], buf, 0)
end)
struct_dict.unpack_from = fn("unpack_from", function(a, kw)
local fmt, buf = a[1], a[2]
local offset = to_num(a[3] or (kw and kw.offset) or 0)
return raw_unpack(fmt, buf, offset)
end)
local StructClass = PyClass.make_class("Struct", {B.object}, {
__init__ = fn("__init__", function(a)
local fmt = a[2]
if type(fmt) == "table" and fmt._data then fmt = fmt._data end
a[1].__dict.format = fmt
a[1].__dict.size = calcsize(fmt)
return B.None
end),
pack = fn("pack", function(a) return raw_pack(a[1].__dict.format, a, 2) end),
unpack = fn("unpack", function(a) return struct_dict.unpack.__call({a[1].__dict.format, a[2]}) end),
unpack_from = fn("unpack_from", function(a, kw) return struct_dict.unpack_from.__call({a[1].__dict.format, a[2], a[3]}, kw) end)
})
struct_dict.Struct = StructClass
return make_namespace("struct", struct_dict)
end
-- ==========================================
-- 2. LOGGING 模組 (支援 stdout 與 mw.log,徹底移除雙重 self 錯誤)
-- ==========================================
function Structes.init_logging()
local log_dict = {
DEBUG = 10, INFO = 20, WARNING = 30, WARN = 30, ERROR = 40, CRITICAL = 50, FATAL = 50, NOTSET = 0
}
local level_names = { [10]="DEBUG", [20]="INFO", [30]="WARNING", [40]="ERROR", [50]="CRITICAL", [0]="NOTSET" }
local LogRecord = PyClass.make_class("LogRecord", {B.object}, {
__init__ = fn("__init__", function(a)
local self, name, lvl, msg = a[1], a[2], to_num(a[3]), a[4]
self.__dict.name = tostring(name or "root")
self.__dict.levelno = lvl
self.__dict.levelname = level_names[lvl] or (" LEVEL " .. tostring(lvl))
self.__dict.msg = RUtils.to_str(msg, false)
return B.None
end),
getMessage = fn("getMessage", function(a) return a[1].__dict.msg end)
})
local Formatter = PyClass.make_class("Formatter", {B.object}, {
__init__ = fn("__init__", function(a, kw)
a[1].__dict.fmt = (a[2] ~= nil and a[2] ~= B.None) and a[2] or (kw and kw.fmt) or "%(levelname)s:%(name)s:%(message)s"
return B.None
end),
format = fn("format", function(a)
local self, record = a[1], a[2]
local s = self.__dict.fmt or "%(levelname)s:%(name)s:%(message)s"
local d = record.__dict
s = s:gsub("%%%(levelname%)s", tostring(d.levelname or "INFO"))
s = s:gsub("%%%(levelno%)s", tostring(d.levelno or 20))
s = s:gsub("%%%(name%)s", tostring(d.name or "root"))
s = s:gsub("%%%(message%)s", tostring(d.msg or ""))
return s
end)
})
local function make_formatter(fmt_str)
local inst = PyClass.make_instance(Formatter)
inst.__dict.fmt = fmt_str or "%(levelname)s:%(name)s:%(message)s"
return inst
end
local function format_record(handler, record)
local fmt_obj = handler.__dict.formatter or make_formatter()
local d = record.__dict
local s = fmt_obj.__dict.fmt or "%(levelname)s:%(name)s:%(message)s"
s = s:gsub("%%%(levelname%)s", tostring(d.levelname or "INFO"))
s = s:gsub("%%%(levelno%)s", tostring(d.levelno or 20))
s = s:gsub("%%%(name%)s", tostring(d.name or "root"))
s = s:gsub("%%%(message%)s", tostring(d.msg or ""))
return s
end
local Handler = PyClass.make_class("Handler", {B.object}, {
__init__ = fn("__init__", function(a)
a[1].__dict.level = to_num(a[2] or 0)
a[1].__dict.formatter = make_formatter()
return B.None
end),
setFormatter = fn("setFormatter", function(a) a[1].__dict.formatter = a[2]; return B.None end),
setLevel = fn("setLevel", function(a) a[1].__dict.level = to_num(a[2]); return B.None end),
format = fn("format", function(a) return format_record(a[1], a[2]) end),
handle = fn("handle", function(a)
local self, record = a[1], a[2]
if to_num(record.__dict.levelno) >= to_num(self.__dict.level) then
local msg = format_record(self, record)
Evaluator.invoke(PyClass.get_attr(self, "emit"), {record, msg}, {})
end
return B.None
end)
})
local StreamHandler = PyClass.make_class("StreamHandler", {Handler}, {
emit = fn("emit", function(a)
local msg = a[3] or format_record(a[1], a[2])
table.insert(Runtime.stdout, tostring(msg) .. "\n")
return B.None
end)
})
local MwLogHandler = PyClass.make_class("MwLogHandler", {Handler}, {
emit = fn("emit", function(a)
local msg = a[3] or format_record(a[1], a[2])
mw.log(tostring(msg))
return B.None
end)
})
local root_logger -- 前向宣告
-- 內部直接發送日誌的核心輔助函式 (完全避開 bound_method 二次綁定問題)
local function emit_log(logger_inst, num_level, raw_msg, extra_args)
local eff_level = to_num(logger_inst.__dict.level)
if eff_level == 0 and root_logger and logger_inst ~= root_logger then
eff_level = to_num(root_logger.__dict.level)
end
if num_level < eff_level then return B.None end
local formatted_msg = RUtils.to_str(raw_msg, false)
if extra_args and #extra_args > 0 then
local ok_mod, mod_res = pcall(function()
local str_mod = PyClass.get_attr(formatted_msg, "__mod__")
if #extra_args == 1 then return str_mod.__call({extra_args[1]})
else return str_mod.__call({{ __type = "tuple", elements = extra_args }}) end
end)
if ok_mod and type(mod_res) == "string" then formatted_msg = mod_res end
end
local rec = PyClass.make_instance(LogRecord)
rec.__dict.name = tostring(logger_inst.__dict.name or "root")
rec.__dict.levelno = num_level
rec.__dict.levelname = level_names[num_level] or ("LVL" .. tostring(num_level))
rec.__dict.msg = formatted_msg
local cur = logger_inst
while cur do
for _, h in ipairs(cur.__dict.handlers or {}) do
if num_level >= to_num(h.__dict.level) then
local out_str = format_record(h, rec)
if h.__class == MwLogHandler then
mw.log(out_str)
elseif h.__class == StreamHandler then
table.insert(Runtime.stdout, out_str .. "\n")
else
Evaluator.invoke(PyClass.get_attr(h, "emit"), {rec, out_str}, {})
end
end
end
if cur.__dict.propagate and cur ~= root_logger then
cur = root_logger
else
break
end
end
return B.None
end
local function collect_extra(a, start_i)
local ex = {}
for i = start_i, #a do table.insert(ex, a[i]) end
return ex
end
local Logger = PyClass.make_class("Logger", {B.object}, {
__init__ = fn("__init__", function(a)
a[1].__dict.name = tostring(a[2] or "root")
a[1].__dict.level = to_num(a[3] or 0)
a[1].__dict.handlers = {}
a[1].__dict.propagate = true
return B.None
end),
setLevel = fn("setLevel", function(a) a[1].__dict.level = to_num(a[2]); return B.None end),
getEffectiveLevel = fn("getEffectiveLevel", function(a)
local lvl = to_num(a[1].__dict.level)
if lvl == 0 and root_logger then return to_num(root_logger.__dict.level) end
return lvl
end),
addHandler = fn("addHandler", function(a) table.insert(a[1].__dict.handlers, a[2]); return B.None end),
log = fn("log", function(a) return emit_log(a[1], to_num(a[2]), a[3], collect_extra(a, 4)) end),
debug = fn("debug", function(a) return emit_log(a[1], 10, a[2], collect_extra(a, 3)) end),
info = fn("info", function(a) return emit_log(a[1], 20, a[2], collect_extra(a, 3)) end),
warning = fn("warning", function(a) return emit_log(a[1], 30, a[2], collect_extra(a, 3)) end),
warn = fn("warn", function(a) return emit_log(a[1], 30, a[2], collect_extra(a, 3)) end),
error = fn("error", function(a) return emit_log(a[1], 40, a[2], collect_extra(a, 3)) end),
critical = fn("critical", function(a) return emit_log(a[1], 50, a[2], collect_extra(a, 3)) end),
fatal = fn("fatal", function(a) return emit_log(a[1], 50, a[2], collect_extra(a, 3)) end),
exception = fn("exception", function(a) return emit_log(a[1], 40, a[2], collect_extra(a, 3)) end)
})
local function new_logger(name, level, propagate)
local l = PyClass.make_instance(Logger)
l.__dict.name = name
l.__dict.level = level or 0
l.__dict.handlers = {}
l.__dict.propagate = (propagate ~= false)
return l
end
local default_handler = PyClass.make_instance(StreamHandler)
default_handler.__dict.level = 0
default_handler.__dict.formatter = make_formatter()
root_logger = new_logger("root", 30, false)
table.insert(root_logger.__dict.handlers, default_handler)
-- 特殊預設 mw logger:直接輸出至維基百科底層 mw.log,且不向 stdout 傳播
local mw_handler = PyClass.make_instance(MwLogHandler)
mw_handler.__dict.level = 0
mw_handler.__dict.formatter = make_formatter()
local mw_logger = new_logger("mw", 10, false)
table.insert(mw_logger.__dict.handlers, mw_handler)
local loggers_registry = { ["root"] = root_logger, ["mw"] = mw_logger }
log_dict.getLogger = fn("getLogger", function(a)
local name = a[1]
if not name or name == B.None or name == "" then return root_logger end
name = tostring(name)
if not loggers_registry[name] then
loggers_registry[name] = new_logger(name, 0, true)
end
return loggers_registry[name]
end)
log_dict.basicConfig = fn("basicConfig", function(a, kw)
kw = kw or {}
if kw.level ~= nil then root_logger.__dict.level = to_num(kw.level) end
if kw.format ~= nil then
root_logger.__dict.handlers[1].__dict.formatter = make_formatter(kw.format)
end
return B.None
end)
-- 模組層級快速日誌函式
log_dict.debug = fn("debug", function(a) return emit_log(root_logger, 10, a[1], collect_extra(a, 2)) end)
log_dict.info = fn("info", function(a) return emit_log(root_logger, 20, a[1], collect_extra(a, 2)) end)
log_dict.warning = fn("warning", function(a) return emit_log(root_logger, 30, a[1], collect_extra(a, 2)) end)
log_dict.warn = log_dict.warning
log_dict.error = fn("error", function(a) return emit_log(root_logger, 40, a[1], collect_extra(a, 2)) end)
log_dict.critical = fn("critical", function(a) return emit_log(root_logger, 50, a[1], collect_extra(a, 2)) end)
log_dict.log = fn("log", function(a) return emit_log(root_logger, to_num(a[1]), a[2], collect_extra(a, 3)) end)
log_dict.Logger = Logger; log_dict.Handler = Handler; log_dict.LogRecord = LogRecord
log_dict.StreamHandler = StreamHandler; log_dict.MwLogHandler = MwLogHandler; log_dict.Formatter = Formatter
return make_namespace("logging", log_dict)
end
-- ==========================================
-- 3. UNITTEST 模組 (完整測試框架與斷言庫)
-- ==========================================
function Structes.init_unittest()
local unit_dict = {}
local AssertionError = B.AssertionError or PyClass.make_class("AssertionError", {B.Exception}, {})
local SkipTest = PyClass.make_class("SkipTest", {B.Exception}, {})
local function fail(msg)
local exc = PyClass.make_instance(AssertionError)
local str_msg = msg and RUtils.to_str(msg, false) or "AssertionError"
exc.__dict._unit_msg = str_msg
PyClass.set_attr(exc, "args", { __type = "tuple", elements = {str_msg} })
Runtime.raise(exc)
end
local function py_contains(member, container)
if type(container) == "table" and container.__type == "instance" then
local ok, attr = pcall(PyClass.get_attr, container, "__contains__")
if ok and attr then
return EvalUtils.is_truthy(Evaluator.invoke(attr, {member}, {}))
end
if container.__builtin_value ~= nil then
container = container.__builtin_value
end
end
if type(member) == "table" and member.__type == "instance" and member.__builtin_value ~= nil then
member = member.__builtin_value
end
if type(container) == "string" then
if type(member) ~= "string" then
Runtime.raise("TypeError", "'in <string>' requires string as left operand")
end
return string.find(container, member, 1, true) ~= nil
elseif type(container) == "table" then
if container.__type == "list" or container.__type == "tuple" or container.__type == "set" or container.__type == "frozenset" then
for _, v in ipairs(container.elements) do
if EvalUtils.py_eq(v, member) then return true end
end
return false
elseif container.__type == "dict" then
for _, p in ipairs(container.pairs) do
if EvalUtils.py_eq(p.key, member) then return true end
end
return false
elseif container.__type == "bytes" or container.__type == "bytearray" or container.__type == "memoryview" then
if type(member) == "number" and math.floor(member) == member then
return string.find(container._data, string.char(member), 1, true) ~= nil
elseif type(member) == "table" and (member.__type == "bytes" or member.__type == "bytearray" or member.__type == "memoryview") then
return string.find(container._data, member._data, 1, true) ~= nil
end
return false
elseif container.__type == "range" then
local n = to_num(member)
if container.step > 0 then return n >= container.start and n < container.stop and (n - container.start) % container.step == 0
else return n <= container.start and n > container.stop and (n - container.start) % container.step == 0 end
end
end
local ok_iter, iter_state = pcall(EvalUtils.make_iter_state, container)
if not ok_iter then
Runtime.raise("TypeError", "argument is not iterable")
end
while true do
local has_next, v = EvalUtils.get_next_item(iter_state)
if not has_next then break end
if EvalUtils.py_eq(v, member) then return true end
end
return false
end
local AssertRaisesContext = PyClass.make_class("_AssertRaisesContext", {B.object}, {
__enter__ = fn("__enter__", function(a) return a[1] end),
__exit__ = fn("__exit__", function(a)
local self, exc_type, exc_val = a[1], a[2], a[3]
local expected = self.__dict.expected
local exp_name = expected.__name or "Exception"
if exc_type == nil or exc_type == B.None then
fail(exp_name .. " not raised")
end
if is_subclass(exc_type, expected) then
self.__dict.exception = exc_val
return true
end
return false
end)
})
local SubTestContext = PyClass.make_class("_SubTest", {B.object}, {
__enter__ = fn("__enter__", function(a) return a[1] end),
__exit__ = fn("__exit__", function() return false end)
})
local TestCase = PyClass.make_class("TestCase", {B.object}, {
__init__ = fn("__init__", function(a) a[1].__dict._testMethodName = a[2] or "runTest"; return B.None end),
setUp = fn("setUp", function() return B.None end),
tearDown = fn("tearDown", function() return B.None end),
fail = fn("fail", function(a) fail(a[2] or "Test failed explicitly") end),
skipTest = fn("skipTest", function(a)
local exc = PyClass.make_instance(SkipTest)
exc.__dict._skip_reason = RUtils.to_str(a[2] or "skipped", false)
Runtime.raise(exc)
end),
subTest = fn("subTest", function() return PyClass.make_instance(SubTestContext) end),
assertEqual = fn("assertEqual", function(a)
if not EvalUtils.py_eq(a[2], a[3]) then
fail(a[4] or (RUtils.to_str(a[2], true) .. " != " .. RUtils.to_str(a[3], true)))
end
return B.None
end),
assertNotEqual = fn("assertNotEqual", function(a)
if EvalUtils.py_eq(a[2], a[3]) then
fail(a[4] or (RUtils.to_str(a[2], true) .. " == " .. RUtils.to_str(a[3], true)))
end
return B.None
end),
assertTrue = fn("assertTrue", function(a)
if not EvalUtils.is_truthy(a[2]) then
fail(a[3] or (RUtils.to_str(a[2], true) .. " is not true"))
end
return B.None
end),
assertFalse = fn("assertFalse", function(a)
if EvalUtils.is_truthy(a[2]) then
fail(a[3] or (RUtils.to_str(a[2], true) .. " is not false"))
end
return B.None
end),
assertIs = fn("assertIs", function(a)
if not EvalUtils.py_is(a[2], a[3]) then
fail(a[4] or (RUtils.to_str(a[2], true) .. " is not " .. RUtils.to_str(a[3], true)))
end
return B.None
end),
assertIsNot = fn("assertIsNot", function(a)
if EvalUtils.py_is(a[2], a[3]) then
fail(a[4] or (RUtils.to_str(a[2], true) .. " is " .. RUtils.to_str(a[3], true)))
end
return B.None
end),
assertIsNone = fn("assertIsNone", function(a)
if a[2] ~= B.None then
fail(a[3] or (RUtils.to_str(a[2], true) .. " is not None"))
end
return B.None
end),
assertIsNotNone = fn("assertIsNotNone", function(a)
if a[2] == B.None then
fail(a[3] or "unexpectedly None")
end
return B.None
end),
assertIn = fn("assertIn", function(a)
if not py_contains(a[2], a[3]) then
fail(a[4] or (RUtils.to_str(a[2], true) .. " not found in " .. RUtils.to_str(a[3], true)))
end
return B.None
end),
assertNotIn = fn("assertNotIn", function(a)
if py_contains(a[2], a[3]) then
fail(a[4] or (RUtils.to_str(a[2], true) .. " unexpectedly found in " .. RUtils.to_str(a[3], true)))
end
return B.None
end),
assertIsInstance = fn("assertIsInstance", function(a)
if not B.isinstance({a[2], a[3]}) then
fail(a[4] or (RUtils.to_str(a[2], true) .. " is not an instance of " .. RUtils.to_str(a[3], true)))
end
return B.None
end),
assertNotIsInstance = fn("assertNotIsInstance", function(a)
if B.isinstance({a[2], a[3]}) then
fail(a[4] or (RUtils.to_str(a[2], true) .. " is an instance of " .. RUtils.to_str(a[3], true)))
end
return B.None
end),
assertAlmostEqual = fn("assertAlmostEqual", function(a, kw)
local x, y = to_num(a[2]), to_num(a[3])
local places = to_num(a[4] or (kw and kw.places) or 7)
local msg = a[5] or (kw and kw.msg)
local delta = kw and kw.delta and to_num(kw.delta) or (10 ^ (-places))
if math.abs(x - y) > delta then
fail(msg or string.format("%s != %s within %s", tostring(x), tostring(y), tostring(delta)))
end
return B.None
end),
assertNotAlmostEqual = fn("assertNotAlmostEqual", function(a, kw)
local x, y = to_num(a[2]), to_num(a[3])
local places = to_num(a[4] or (kw and kw.places) or 7)
local msg = a[5] or (kw and kw.msg)
local delta = kw and kw.delta and to_num(kw.delta) or (10 ^ (-places))
if math.abs(x - y) <= delta then
fail(msg or string.format("%s == %s within %s", tostring(x), tostring(y), tostring(delta)))
end
return B.None
end),
assertGreater = fn("assertGreater", function(a)
if not (EvalUtils.py_cmp(a[2], a[3]) > 0) then fail(a[4] or (RUtils.to_str(a[2], true) .. " not greater than " .. RUtils.to_str(a[3], true))) end
return B.None
end),
assertGreaterEqual = fn("assertGreaterEqual", function(a)
if not (EvalUtils.py_cmp(a[2], a[3]) >= 0) then fail(a[4] or (RUtils.to_str(a[2], true) .. " not greater than or equal to " .. RUtils.to_str(a[3], true))) end
return B.None
end),
assertLess = fn("assertLess", function(a)
if not (EvalUtils.py_cmp(a[2], a[3]) < 0) then fail(a[4] or (RUtils.to_str(a[2], true) .. " not less than " .. RUtils.to_str(a[3], true))) end
return B.None
end),
assertLessEqual = fn("assertLessEqual", function(a)
if not (EvalUtils.py_cmp(a[2], a[3]) <= 0) then fail(a[4] or (RUtils.to_str(a[2], true) .. " not less than or equal to " .. RUtils.to_str(a[3], true))) end
return B.None
end),
assertRaises = fn("assertRaises", function(a, kw)
local exc_cls = a[2]
if #a == 2 and (not kw or kw.callable == nil) then
local ctx = PyClass.make_instance(AssertRaisesContext)
ctx.__dict.expected = exc_cls
return ctx
end
local callable = a[3] or (kw and kw.callable)
local call_args = {}
for i = 4, #a do table.insert(call_args, a[i]) end
local ok, res = pcall(Evaluator.invoke, callable, call_args, kw or {})
if ok then fail((exc_cls.__name or "Exception") .. " not raised") end
if type(res) == "table" and res.__class then
if not is_subclass(res.__class, exc_cls) then
fail("Expected " .. tostring(exc_cls.__name) .. " but got " .. tostring(res.__class.__name))
end
end
return B.None
end)
})
unit_dict.TestCase = TestCase
unit_dict.SkipTest = SkipTest
unit_dict.skip = fn("skip", function(a)
local reason = a[1] or "skipped"
return fn("skip_deco", function(da)
local target = da[1]
if type(target) == "table" then target.__unittest_skip__ = reason end
return target
end)
end)
unit_dict.skipIf = fn("skipIf", function(a)
if EvalUtils.is_truthy(a[1]) then return unit_dict.skip.__call({a[2]}) end
return fn("noop_deco", function(da) return da[1] end)
end)
unit_dict.skipUnless = fn("skipUnless", function(a)
if not EvalUtils.is_truthy(a[1]) then return unit_dict.skip.__call({a[2]}) end
return fn("noop_deco", function(da) return da[1] end)
end)
unit_dict.main = fn("main", function()
local scope = Runtime.Traceback.current_scope
while scope and scope.parent do scope = scope.parent end
local test_classes = {}
for k, v in pairs(scope and scope.vars or {}) do
if type(v) == "table" and v.__type == "class" and v ~= TestCase and is_subclass(v, TestCase) then
table.insert(test_classes, {name = k, cls = v})
end
end
table.sort(test_classes, function(x, y) return x.name < y.name end)
local total_run, total_fail, total_err, total_skip = 0, 0, 0, 0
local report = {}
local progress = {}
local start_clock = os.clock() or 0
for _, tc in ipairs(test_classes) do
local test_names = {}
for k, v in pairs(tc.cls.__dict) do
if type(k) == "string" and k:match("^test") and type(v) == "table" and (v.__type == "function" or v.__type == "builtin_method") then
table.insert(test_names, k)
end
end
table.sort(test_names)
for _, t_name in ipairs(test_names) do
total_run = total_run + 1
local raw_fn = tc.cls.__dict[t_name]
if tc.cls.__unittest_skip__ or ( raw_fn and raw_fn.__unittest_skip__ ) then
total_skip = total_skip + 1
table.insert(progress, "s")
else
local inst = PyClass.make_instance(tc.cls)
inst.__dict._testMethodName = t_name
local ok_setup, res_setup = pcall(Evaluator.invoke, PyClass.get_attr(inst, "setUp"), {}, {})
if not ok_setup then
total_err = total_err + 1
table.insert(progress, "E")
table.insert(report, string.format("ERROR: %s (%s)\n%s", t_name, tc.name, RUtils.to_str(res_setup, false)))
else
local method = PyClass.get_attr(inst, t_name)
local ok_test, res_test = pcall(Evaluator.invoke, method, {}, {})
if ok_test then
table.insert(progress, ".")
else
if type(res_test) == "table" and res_test.__class == SkipTest then
total_skip = total_skip + 1
table.insert(progress, "s")
elseif type(res_test) == "table" and (res_test.__class == AssertionError or (res_test.__class and res_test.__class.__name == "AssertionError")) then
total_fail = total_fail + 1
table.insert(progress, "F")
local f_msg = (res_test.__dict and res_test.__dict._unit_msg)
or (res_test.__dict and res_test.__dict.args and res_test.__dict.args.elements and res_test.__dict.args.elements[1])
or RUtils.to_str(res_test, false)
table.insert(report, string.format("FAIL: %s (%s)\nAssertionError: %s", t_name, tc.name, tostring(f_msg)))
else
total_err = total_err + 1
table.insert(progress, "E")
local e_name = (type(res_test) == "table" and res_test.__class and res_test.__class.__name) or "Error"
table.insert(report, string.format("ERROR: %s (%s)\n%s: %s", t_name, tc.name, e_name, RUtils.to_str(res_test, false)))
end
end
pcall(Evaluator.invoke, PyClass.get_attr(inst, "tearDown"), {}, {})
end
end
end
end
local elapsed = math.max(0, (os.clock() or 0) - start_clock)
if #progress > 0 then
table.insert(Runtime.stdout, table.concat(progress) .. "\n")
end
if #report > 0 then
for _, r in ipairs(report) do
table.insert(Runtime.stdout, string.rep("=", 40) .. "\n" .. r .. "\n")
end
end
table.insert(Runtime.stdout, string.rep("-", 40) .. "\n")
table.insert(Runtime.stdout, string.format("Ran %d test%s in %.3fs\n\n", total_run, total_run == 1 and "" or "s", elapsed))
if total_fail == 0 and total_err == 0 then
local extra = total_skip > 0 and string.format(" (skipped=%d)", total_skip) or ""
table.insert(Runtime.stdout, "OK" .. extra .. "\n")
else
table.insert(Runtime.stdout, string.format("FAILED (failures=%d, errors=%d, skipped=%d)\n", total_fail, total_err, total_skip))
end
return B.None
end)
return make_namespace("unittest", unit_dict)
end
-- ==========================================
-- 4. CALENDAR 模組 (支援 i18n 語系、自訂繼承 Hook 與 Wiki 表格)
-- ==========================================
function Structes.init_calendar()
local cal_dict = {
MONDAY = 0, TUESDAY = 1, WEDNESDAY = 2, THURSDAY = 3, FRIDAY = 4, SATURDAY = 5, SUNDAY = 6
}
local LOCALES = {
en = {
months = {"January","February","March","April","May","June","July","August","September","October","November","December"},
days = {"Mo","Tu","We","Th","Fr","Sa","Su"},
title = "{month} {year}"
},
zh = {
months = {"一月","二月","三月","四月","五月","六月","七月","八月","九月","十月","十一月","十二月"},
days = {"一","二","三","四","五","六","日"},
title = "{year}年 {month}"
},
zh_full = {
months = {"一月","二月","三月","四月","五月","六月","七月","八月","九月","十月","十一月","十二月"},
days = {"星期一","星期二","星期三","星期四","星期五","星期六","星期日"},
title = "{year}年 {month}"
},
ja = {
months = {"1月","2月","3月","4月","5月","6月","7月","8月","9月","10月","11月","12月"},
days = {"月","火","水","木","金","土","日"},
title = "{year}年 {month}"
}
}
local m_names = LOCALES.en.months
local m_abbrs = {"Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"}
local d_names = {"Monday","Tuesday","Wednesday","Thursday","Friday","Saturday","Sunday"}
local d_abbrs = {"Mon","Tue","Wed","Thu","Fri","Sat","Sun"}
cal_dict.month_name = { __type = "list", elements = {"", unpack(m_names)} }
cal_dict.month_abbr = { __type = "list", elements = {"", unpack(m_abbrs)} }
cal_dict.day_name = { __type = "list", elements = d_names }
cal_dict.day_abbr = { __type = "list", elements = d_abbrs }
local function isleap(year)
year = to_num(year)
return year % 4 == 0 and (year % 100 ~= 0 or year % 400 == 0)
end
local function leapdays(y1, y2)
y1, y2 = to_num(y1), to_num(y2)
local function leaps_before(y) y = y - 1; return math.floor(y/4) - math.floor(y/100) + math.floor(y/400) end
return leaps_before(y2) - leaps_before(y1)
end
local function weekday(year, month, day)
year, month, day = to_num(year), to_num(month), to_num(day)
local days = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30}
local y = year - 1
local d = y * 365 + math.floor(y/4) - math.floor(y/100) + math.floor(y/400)
for i = 1, month - 1 do
d = d + days[i + 1]
if i == 2 and isleap(year) then d = d + 1 end
end
d = d + day
return (d + 6) % 7
end
local function raw_monthrange(year, month)
year, month = to_num(year), to_num(month)
if month < 1 or month > 12 then Runtime.raise("ValueError", "bad month number; must be 1-12") end
local days = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}
local d = days[month]
if month == 2 and isleap(year) then d = 29 end
return weekday(year, month, 1), d
end
local function raw_monthcalendar(year, month, firstweekday)
firstweekday = to_num(firstweekday or 0) % 7
local first_wd, n_days = raw_monthrange(year, month)
local lead_empty = (first_wd - firstweekday) % 7
local weeks, cur_week = {}, {}
for _ = 1, lead_empty do table.insert(cur_week, 0) end
for d = 1, n_days do
table.insert(cur_week, d)
if #cur_week == 7 then table.insert(weeks, cur_week); cur_week = {} end
end
if #cur_week > 0 then
while #cur_week < 7 do table.insert(cur_week, 0) end
table.insert(weeks, cur_week)
end
return weeks
end
-- 💡 i18n 解析引擎:整合實例屬性、繼承類別屬性、Hook 方法與 kwargs
local function resolve_i18n(self_obj, kw)
kw = kw or {}
local function get_opt(k1, k2)
if kw[k1] ~= nil and kw[k1] ~= B.None then return kw[k1] end
if k2 and kw[k2] ~= nil and kw[k2] ~= B.None then return kw[k2] end
if self_obj then
local ok1, v1 = pcall(PyClass.get_attr, self_obj, k1)
if ok1 and v1 ~= nil and v1 ~= B.None then return v1 end
if k2 then
local ok2, v2 = pcall(PyClass.get_attr, self_obj, k2)
if ok2 and v2 ~= nil and v2 ~= B.None then return v2 end
end
end
return nil
end
local loc_name = get_opt("locale", "__locale__")
loc_name = loc_name and RUtils.to_str(loc_name, false):lower() or "en"
local preset = LOCALES[loc_name] or LOCALES.en
local custom_months = get_opt("months", "month_names") or get_opt("__month_names__")
local custom_days = get_opt("days", "day_names") or get_opt("weekdays", "__day_names__")
local title_fmt = get_opt("title_fmt", "__title_fmt__")
title_fmt = title_fmt and RUtils.to_str(title_fmt, false) or preset.title
local function extract_list(src, expected_len)
if not src then return nil end
if type(src) == "table" and (src.__type == "list" or src.__type == "tuple") then
local elts = src.elements
local offset = (#elts == expected_len + 1 and RUtils.to_str(elts[1], false) == "") and 1 or 0
local res = {}
for i = 1, expected_len do
res[i] = elts[i + offset] and RUtils.to_str(elts[i + offset], false) or ""
end
return res
elseif type(src) == "table" and src.__type == "dict" then
local res = {}
for _, p in ipairs(src.pairs) do res[to_num(p.key)] = RUtils.to_str(p.value, false) end
return res
end
return nil
end
local m_list = extract_list(custom_months, 12) or preset.months
local d_list = extract_list(custom_days, 7) or preset.days
return {
get_title = function(y, m)
if self_obj then
local ok, fn_m = pcall(PyClass.get_attr, self_obj, "formatmonthname")
if ok and fn_m then
return RUtils.to_str(Evaluator.invoke(fn_m, {y, m}, {}), false)
end
end
local m_str = m_list[m] or tostring(m)
return (title_fmt:gsub("{year}", tostring(y)):gsub("{month}", m_str):gsub("{month_num}", tostring(m)))
end,
get_weekday = function(wd)
if self_obj then
local ok, fn_w = pcall(PyClass.get_attr, self_obj, "formatweekday")
if ok and fn_w then
return RUtils.to_str(Evaluator.invoke(fn_w, {wd}, {}), false)
end
end
return d_list[wd + 1] or d_names[wd + 1]
end
}
end
local function format_text_month(year, month, firstweekday, self_obj, kw)
year, month = to_num(year), to_num(month)
firstweekday = to_num(firstweekday or (kw and kw.firstweekday) or 0) % 7
local i18n = resolve_i18n(self_obj, kw)
local header = i18n.get_title(year, month)
local pad = math.floor((20 - mw.ustring.len(header)) / 2)
local wk_hdr = {}
for i = 0, 6 do table.insert(wk_hdr, i18n.get_weekday((firstweekday + i) % 7)) end
local lines = {
string.rep(" ", math.max(0, pad)) .. header,
table.concat(wk_hdr, " ")
}
local weeks = raw_monthcalendar(year, month, firstweekday)
for _, wk in ipairs(weeks) do
local row = {}
for _, d in ipairs(wk) do
table.insert(row, d == 0 and " " or string.format("%2d", d))
end
table.insert(lines, table.concat(row, " "))
end
return table.concat(lines, "\n") .. "\n"
end
local function format_wiki_month(year, month, firstweekday, events_dict, self_obj, kw)
year, month = to_num(year), to_num(month)
firstweekday = to_num(firstweekday or (kw and kw.firstweekday) or 0) % 7
local i18n = resolve_i18n(self_obj, kw)
local events = {}
local ev_src = events_dict or (kw and kw.events)
if type(ev_src) == "table" and ev_src.__type == "dict" then
for _, p in ipairs(ev_src.pairs) do
events[to_num(p.key)] = RUtils.to_str(p.value, false)
end
end
local out = {
'{| class="wikitable" style="text-align:center; min-width:280px; border-collapse:collapse;"',
'|-',
string.format('! colspan="7" style="background:#eaecf0; font-size:110%%; padding:6px;" | %s', i18n.get_title(year, month)),
'|-'
}
local hdr_cells = {}
for i = 0, 6 do
local wd = (firstweekday + i) % 7
local label = i18n.get_weekday(wd)
if wd == 6 then
table.insert(hdr_cells, 'style="background:#f8f9fa; color:#d33;" | ' .. label)
elseif wd == 5 then
table.insert(hdr_cells, 'style="background:#f8f9fa; color:#06c;" | ' .. label)
else
table.insert(hdr_cells, 'style="background:#f8f9fa;" | ' .. label)
end
end
table.insert(out, '! ' .. table.concat(hdr_cells, ' !! '))
local weeks = raw_monthcalendar(year, month, firstweekday)
for _, wk in ipairs(weeks) do
table.insert(out, '|-')
local row_cells = {}
local i = 1
while i <= 7 do
local d = wk[i]
if d == 0 then
local span = 1
while (i + span <= 7) and wk[i + span] == 0 do span = span + 1 end
if span > 1 then
table.insert(row_cells, string.format('colspan="%d" style="background:#f8f9fa;" | ', span))
else
table.insert(row_cells, 'style="background:#f8f9fa;" | ')
end
i = i + span
else
local wd = (firstweekday + i - 1) % 7
local ev = events[d]
local styles = {}
if ev then
table.insert(styles, "background:#fff8dc; font-weight:bold;")
elseif wd == 6 then
table.insert(styles, "color:#d33;")
elseif wd == 5 then
table.insert(styles, "color:#06c;")
end
local cell_text = ev and string.format("%d<br /><small>%s</small>", d, ev) or tostring(d)
if #styles > 0 then
table.insert(row_cells, string.format('style="%s" | %s', table.concat(styles, " "), cell_text))
else
table.insert(row_cells, cell_text)
end
i = i + 1
end
end
table.insert(out, '| ' .. table.concat(row_cells, ' || '))
end
table.insert(out, '|}')
return table.concat(out, '\n')
end
local function format_wiki_year(year, cols, firstweekday, self_obj, kw)
year = to_num(year)
cols = math.max(1, to_num(cols or (kw and kw.cols) or 3))
firstweekday = to_num(firstweekday or (kw and kw.firstweekday) or 0) % 7
local out = {
'{| class="wikitable" style="text-align:center; background:#ffffff;"',
'|-',
string.format('! colspan="%d" style="background:#d8e2ef; font-size:130%%; padding:8px;" | %d', cols, year)
}
for m = 1, 12 do
if (m - 1) % cols == 0 then table.insert(out, '|- style="vertical-align:top;"') end
table.insert(out, '| style="padding:6px;" |\n' .. format_wiki_month(year, m, firstweekday, nil, self_obj, kw))
end
table.insert(out, '|}')
return table.concat(out, '\n')
end
cal_dict.isleap = fn("isleap", function(a) return isleap(a[1]) end)
cal_dict.leapdays = fn("leapdays", function(a) return leapdays(a[1], a[2]) end)
cal_dict.weekday = fn("weekday", function(a) return weekday(a[1], a[2], a[3]) end)
cal_dict.monthrange = fn("monthrange", function(a)
local fw, nd = raw_monthrange(a[1], a[2])
return { __type = "tuple", elements = {fw, nd} }
end)
cal_dict.monthcalendar = fn("monthcalendar", function(a)
local weeks = raw_monthcalendar(a[1], a[2], 0)
local py_weeks = {}
for _, wk in ipairs(weeks) do table.insert(py_weeks, { __type = "list", elements = wk }) end
return { __type = "list", elements = py_weeks }
end)
local TextCalendar = PyClass.make_class("TextCalendar", {B.object}, {
__init__ = fn("__init__", function(a, kw)
a[1].__dict.firstweekday = to_num(a[2] or (kw and kw.firstweekday) or 0) % 7
if kw then for k, v in pairs(kw) do a[1].__dict[k] = v end end
return B.None
end),
formatmonth = fn("formatmonth", function(a, kw)
return format_text_month(a[2], a[3], a[1].__dict.firstweekday, a[1], kw)
end),
prmonth = fn("prmonth", function(a, kw)
table.insert(Runtime.stdout, format_text_month(a[2], a[3], a[1].__dict.firstweekday, a[1], kw))
return B.None
end)
})
local WikiCalendar = PyClass.make_class("WikiCalendar", {B.object}, {
__init__ = fn("__init__", function(a, kw)
a[1].__dict.firstweekday = to_num(a[2] or (kw and kw.firstweekday) or 0) % 7
if kw then for k, v in pairs(kw) do a[1].__dict[k] = v end end
return B.None
end),
formatmonth = fn("formatmonth", function(a, kw)
local ev = a[4] or (kw and kw.events)
return format_wiki_month(a[2], a[3], a[1].__dict.firstweekday, ev, a[1], kw)
end),
prmonth = fn("prmonth", function(a, kw)
local ev = a[4] or (kw and kw.events)
table.insert(Runtime.stdout, format_wiki_month(a[2], a[3], a[1].__dict.firstweekday, ev, a[1], kw) .. "\n")
return B.None
end),
formatyear = fn("formatyear", function(a, kw)
local cols = a[3] or (kw and kw.cols) or 3
return format_wiki_year(a[2], cols, a[1].__dict.firstweekday, a[1], kw)
end)
})
cal_dict.TextCalendar = TextCalendar
cal_dict.WikiCalendar = WikiCalendar
cal_dict.month = fn("month", function(a, kw) return format_text_month(a[1], a[2], 0, nil, kw) end)
cal_dict.prmonth = fn("prmonth", function(a, kw) table.insert(Runtime.stdout, format_text_month(a[1], a[2], 0, nil, kw)); return B.None end)
cal_dict.wikimonth = fn("wikimonth", function(a, kw)
local fw = a[3] or (kw and kw.firstweekday) or 0
local ev = a[4] or (kw and kw.events)
return format_wiki_month(a[1], a[2], fw, ev, nil, kw)
end)
cal_dict.wikiyear = fn("wikiyear", function(a, kw)
local cols = a[2] or (kw and kw.cols) or 3
local fw = a[3] or (kw and kw.firstweekday) or 0
return format_wiki_year(a[1], cols, fw, nil, kw)
end)
return make_namespace("calendar", cal_dict)
end
return Structes