feat: support user-defined functions and classes in Ren'Py python
- Converter: def/return/global/nonlocal, class (inheritance, property, staticmethod, classmethod), lambda, *args/**kwargs and call-site unpacking. Unsupported dunder methods, generators and unknown decorators are still refused - Player: closures, super(), instances that survive save/load, isinstance, hasattr/getattr/setattr, map/filter/sorted(key=), recursion limit - audit_ir accepts calls to names, classes and methods the game defines itself; unknown renpy.* calls still block the import. Volume/pan/pause/replay calls are accepted as presentation-only no-ops; build.* settings are skipped - Tests: IR fixture compiled from tests/fixtures/classes.py (tests/make_ir.py) and run in the interpreter; converter tests updated Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
12ec82c7e6
commit
962595c925
12 files changed
+782
-23
No files matched your search
@@ -105,10 +105,13 @@ def expr(node: ast.AST) -> Expr:
|
||||
raise Unsupported("распаковка ** в словаре")
|
||||
return {"k": "dict", "v": [[expr(k), expr(v)] for k, v in zip(keys, values)]}
|
||||
case ast.Call(func=func, args=args, keywords=kws):
|
||||
if any(isinstance(a, ast.Starred) for a in args) or any(k.arg is None for k in kws):
|
||||
raise Unsupported("распаковка аргументов *args/**kwargs")
|
||||
return {"k": "call", "f": expr(func), "a": [expr(a) for a in args],
|
||||
"kw": {k.arg: expr(k.value) for k in kws}}
|
||||
call = {"k": "call", "f": expr(func),
|
||||
"a": [{"k": "star", "v": expr(a.value)} if isinstance(a, ast.Starred) else expr(a)
|
||||
for a in args],
|
||||
"kw": {k.arg: expr(k.value) for k in kws if k.arg is not None}}
|
||||
if any(k.arg is None for k in kws):
|
||||
call["ks"] = [expr(k.value) for k in kws if k.arg is None] # **mapping, in order
|
||||
return call
|
||||
case ast.JoinedStr(values=values):
|
||||
parts: list[Any] = []
|
||||
for v in values:
|
||||
@@ -125,8 +128,8 @@ def expr(node: ast.AST) -> Expr:
|
||||
is_async=0)])):
|
||||
return {"k": "comp", "e": expr(elt), "t": target(t), "it": expr(it),
|
||||
"if": [expr(i) for i in ifs]}
|
||||
case ast.Lambda():
|
||||
raise Unsupported("lambda")
|
||||
case ast.Lambda(args=args, body=body):
|
||||
return {"k": "lambda", "params": params(args), "body": expr(body)}
|
||||
raise Unsupported(f"выражение {type(node).__name__}")
|
||||
|
||||
|
||||
@@ -173,7 +176,7 @@ def stmt(node: ast.stmt) -> list[Stmt]:
|
||||
return [{"op": "for", "t": target(t), "it": expr(it), "body": stmts(body)}]
|
||||
case ast.Delete(targets=targets):
|
||||
return [{"op": "del", "t": target(t)} for t in targets]
|
||||
case ast.Pass() | ast.Global() | ast.Nonlocal():
|
||||
case ast.Pass():
|
||||
return []
|
||||
case ast.Break():
|
||||
return [{"op": "break"}]
|
||||
@@ -184,6 +187,64 @@ def stmt(node: ast.stmt) -> list[Stmt]:
|
||||
if mod.split(".")[0] in KNOWN_MODULES:
|
||||
return []
|
||||
raise Unsupported(f"импорт модуля {mod}")
|
||||
case ast.FunctionDef(name=name) | ast.ClassDef(name=name):
|
||||
raise Unsupported(f"определение {name}()")
|
||||
case ast.FunctionDef(name=name, args=args, body=body, decorator_list=decos):
|
||||
return [{"op": "def", "name": name, "params": params(args), "body": function_body(body),
|
||||
"decos": [decorator(d) for d in decos]}]
|
||||
case ast.ClassDef(name=name, bases=bases, keywords=[], body=body, decorator_list=[]):
|
||||
return [{"op": "class", "name": name, "bases": [expr(b) for b in bases],
|
||||
"body": class_body(name, body)}]
|
||||
case ast.ClassDef(name=name):
|
||||
raise Unsupported(f"определение класса {name} с декораторами или метаклассом")
|
||||
case ast.Return(value=value):
|
||||
return [{"op": "return", "v": None if value is None else expr(value)}]
|
||||
case ast.Global(names=names):
|
||||
return [{"op": "global", "names": list(names)}]
|
||||
case ast.Nonlocal(names=names):
|
||||
return [{"op": "nonlocal", "names": list(names)}]
|
||||
raise Unsupported(f"оператор {type(node).__name__}")
|
||||
|
||||
|
||||
# Methods the player can run implicitly. Any other dunder method would silently
|
||||
# never be called, so the converter refuses it instead.
|
||||
SUPPORTED_DUNDERS = {"__init__", "__str__", "__repr__"}
|
||||
DECORATORS = {"staticmethod", "classmethod", "property"}
|
||||
|
||||
|
||||
def params(args: ast.arguments) -> list[dict[str, Any]]:
|
||||
"""Parameter list: kind ``p`` positional/keyword, ``k`` keyword-only, ``*`` and ``**`` collectors."""
|
||||
out: list[dict[str, Any]] = []
|
||||
positional = [*args.posonlyargs, *args.args]
|
||||
defaults = [None] * (len(positional) - len(args.defaults)) + list(args.defaults)
|
||||
for a, d in zip(positional, defaults):
|
||||
out.append({"n": a.arg, "d": None if d is None else expr(d), "kind": "p"})
|
||||
if args.vararg:
|
||||
out.append({"n": args.vararg.arg, "d": None, "kind": "*"})
|
||||
for a, d in zip(args.kwonlyargs, args.kw_defaults):
|
||||
out.append({"n": a.arg, "d": None if d is None else expr(d), "kind": "k"})
|
||||
if args.kwarg:
|
||||
out.append({"n": args.kwarg.arg, "d": None, "kind": "**"})
|
||||
return out
|
||||
|
||||
|
||||
def decorator(node: ast.expr) -> Expr:
|
||||
match node:
|
||||
case ast.Name(id=name) if name in DECORATORS:
|
||||
return {"k": "name", "id": name}
|
||||
case ast.Attribute(value=ast.Name(), attr="setter"):
|
||||
return expr(node)
|
||||
raise Unsupported(f"декоратор {ast.unparse(node)}")
|
||||
|
||||
|
||||
def function_body(body: list[ast.stmt]) -> list[Stmt]:
|
||||
for n in ast.walk(ast.Module(body=body, type_ignores=[])):
|
||||
if isinstance(n, (ast.Yield, ast.YieldFrom, ast.Await)):
|
||||
raise Unsupported("генераторы и async")
|
||||
return stmts(body)
|
||||
|
||||
|
||||
def class_body(name: str, body: list[ast.stmt]) -> list[Stmt]:
|
||||
for n in body:
|
||||
if (isinstance(n, ast.FunctionDef) and n.name.startswith("__") and n.name.endswith("__")
|
||||
and n.name not in SUPPORTED_DUNDERS):
|
||||
raise Unsupported(f"специальный метод {name}.{n.name}()")
|
||||
return stmts(body)
|
||||
Reference in new issue
Block a user