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
@@ -0,0 +1,17 @@
|
||||
"""Compile a Python file to PGVN IR JSON: python -m tests.make_ir SOURCE.py OUT.json"""
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from app import pgvn
|
||||
from app.converter import pyexpr
|
||||
|
||||
|
||||
def main(source: Path, out: Path) -> None:
|
||||
code = pyexpr.parse_stmts(source.read_text(encoding="utf-8"))
|
||||
pgvn.statements(code)
|
||||
out.write_text(json.dumps(code, ensure_ascii=False, separators=(",", ":")), encoding="utf-8")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main(Path(sys.argv[1]), Path(sys.argv[2]))
|
||||
@@ -78,7 +78,7 @@ def test_renpy_only_declarations_are_not_execution_blockers(tmp_path):
|
||||
|
||||
|
||||
@pytest.mark.parametrize("source,reason", [
|
||||
('init python:\n def helper():\n pass\nlabel start:\n "Hi"', "init python"),
|
||||
('init python:\n def helper():\n yield 1\nlabel start:\n "Hi"', "init python"),
|
||||
('label start:\n $ result = renpy.show_screen("map")', "renpy.show_screen"),
|
||||
('label start:\n jump missing', "missing"),
|
||||
])
|
||||
|
||||
@@ -29,7 +29,7 @@ def test_expr_translation():
|
||||
e = pyexpr.parse_expr("a >= 1 and 'x' in inv or not flag")
|
||||
assert e["k"] == "bool" and e["op"] == "or"
|
||||
with pytest.raises(pyexpr.Unsupported):
|
||||
pyexpr.parse_expr("lambda: 1")
|
||||
pyexpr.parse_expr("(y := 1)")
|
||||
|
||||
|
||||
def test_say_variants():
|
||||
@@ -57,10 +57,43 @@ def test_if_chain_and_menu():
|
||||
|
||||
|
||||
def test_unsupported_python_is_reported_not_fatal():
|
||||
d = parse("init python:\n def f():\n pass\n x = 1\nlabel start:\n $ y = x\n")
|
||||
d = parse("init python:\n def f():\n yield 1\n x = 1\nlabel start:\n $ y = x\n")
|
||||
assert d.warnings and d.init_code[0]["op"] == "set"
|
||||
|
||||
|
||||
def test_user_functions_and_classes_are_interpreted_not_rejected():
|
||||
from app.converter.compatibility import audit_ir
|
||||
|
||||
d = parse(
|
||||
"init python:\n"
|
||||
" class Mood(object):\n"
|
||||
" def __init__(self, v=0):\n"
|
||||
" self.v = v\n"
|
||||
" def up(self, n=1):\n"
|
||||
" self.v += n\n"
|
||||
" return self\n"
|
||||
" def bonus(m, *rest, **kw):\n"
|
||||
" return m.up(2).v\n"
|
||||
"default mood = Mood()\n"
|
||||
"label start:\n"
|
||||
" $ x = bonus(mood)\n"
|
||||
" $ mood.up()\n")
|
||||
audit_ir(d)
|
||||
assert not d.warnings and not d.blockers
|
||||
assert [s["op"] for s in d.init_code] == ["class", "def"]
|
||||
|
||||
|
||||
def test_unknown_renpy_calls_and_unsupported_class_features_are_still_reported():
|
||||
from app.converter.compatibility import audit_ir
|
||||
|
||||
d = parse("label start:\n $ renpy.no_such_call()\n $ missing_function()\n")
|
||||
audit_ir(d)
|
||||
messages = " ".join(d.warnings)
|
||||
assert "renpy.no_such_call" in messages and "missing_function" in messages
|
||||
d = parse("init python:\n class A(object):\n def __eq__(self, other):\n return True\n")
|
||||
assert any("__eq__" in w for w in d.warnings)
|
||||
|
||||
|
||||
def test_full_pipeline(data_dir, tmp_path):
|
||||
archive = build(tmp_path / "sample.zip")
|
||||
steps = []
|
||||
|
||||
Reference in new issue
Block a user