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
Vendored
+103
@@ -0,0 +1,103 @@
|
||||
# Source of classes.ir.json. Regenerate from backend/ with:
|
||||
# python -m tests.make_ir ../frontend/tests/fixtures/classes.py ../frontend/tests/fixtures/classes.ir.json
|
||||
# The Python below is also valid CPython, so expected values can be checked there.
|
||||
|
||||
|
||||
class Perk(object):
|
||||
count = 0
|
||||
|
||||
def __init__(self, name, cost=1):
|
||||
self.name = name
|
||||
self.cost = cost
|
||||
Perk.count += 1
|
||||
|
||||
def __str__(self):
|
||||
return "Perk(%s)" % self.name
|
||||
|
||||
|
||||
class Player(object):
|
||||
def __init__(self, name, *perks, **extra):
|
||||
self.name = name
|
||||
self.perks = list(perks)
|
||||
self.extra = extra
|
||||
self._hp = 10
|
||||
|
||||
def has_perk(self, name):
|
||||
return any([p.name == name for p in self.perks])
|
||||
|
||||
def add(self, perk):
|
||||
self.perks.append(perk)
|
||||
return self
|
||||
|
||||
@property
|
||||
def hp(self):
|
||||
return self._hp
|
||||
|
||||
@hp.setter
|
||||
def hp(self, value):
|
||||
self._hp = max(0, value)
|
||||
|
||||
@staticmethod
|
||||
def make(name):
|
||||
return Player(name)
|
||||
|
||||
@classmethod
|
||||
def label(cls):
|
||||
return cls is Player
|
||||
|
||||
|
||||
class Hero(Player):
|
||||
def __init__(self, name):
|
||||
super().__init__(name, Perk("brave"), role="hero")
|
||||
self.level = 1
|
||||
|
||||
def has_perk(self, name):
|
||||
return name == "legend" or super().has_perk(name)
|
||||
|
||||
|
||||
total = 0
|
||||
|
||||
|
||||
def bump(step=1, *, scale=2):
|
||||
global total
|
||||
total += step * scale
|
||||
return total
|
||||
|
||||
|
||||
def fact(n):
|
||||
return 1 if n <= 1 else n * fact(n - 1)
|
||||
|
||||
|
||||
def counter():
|
||||
n = 0
|
||||
|
||||
def inc():
|
||||
nonlocal n
|
||||
n += 1
|
||||
return n
|
||||
|
||||
return inc
|
||||
|
||||
|
||||
hero = Hero("Ann")
|
||||
hero.add(Perk("fast", cost=3))
|
||||
hero.hp = -5
|
||||
call_args = [2]
|
||||
call_kw = {"scale": 10}
|
||||
result_brave = hero.has_perk("brave")
|
||||
result_legend = hero.has_perk("legend")
|
||||
result_missing = hero.has_perk("nope")
|
||||
result_count = Perk.count
|
||||
result_str = str(Perk("x"))
|
||||
result_hp = hero.hp
|
||||
result_role = hero.extra["role"]
|
||||
result_static = Player.make("Bob").name
|
||||
result_cls = Player.label()
|
||||
result_bump = bump(*call_args, **call_kw)
|
||||
result_total = total
|
||||
result_fact = fact(6)
|
||||
tick = counter()
|
||||
tick()
|
||||
result_closure = tick()
|
||||
result_lambda = sorted([3, 1, 2], key=lambda v: -v)
|
||||
result_isinstance = [isinstance(hero, Player), isinstance(hero, Perk), isinstance(1, int)]
|
||||
Reference in new issue
Block a user