Skip to content

Commit 02c0780

Browse files
committed
improve class loading (dataclass, enum, metaclass, keywords, etc).
1 parent d1ddf37 commit 02c0780

File tree

2 files changed

+90
-6
lines changed

2 files changed

+90
-6
lines changed

custom_components/pyscript/eval.py

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1091,12 +1091,18 @@ async def ast_while(self, arg):
10911091
async def ast_classdef(self, arg):
10921092
"""Evaluate class definition."""
10931093
bases = [(await self.aeval(base)) for base in arg.bases]
1094+
keywords = {kw.arg: await self.aeval(kw.value) for kw in arg.keywords}
1095+
metaclass = keywords.pop("metaclass", type(bases[0]) if bases else type)
1096+
10941097
if self.curr_func and arg.name in self.curr_func.global_names:
10951098
sym_table_assign = self.global_sym_table
10961099
else:
10971100
sym_table_assign = self.sym_table
10981101
sym_table_assign[arg.name] = EvalLocalVar(arg.name)
1099-
sym_table = {}
1102+
if hasattr(metaclass, "__prepare__"):
1103+
sym_table = metaclass.__prepare__(arg.name, tuple(bases), **keywords)
1104+
else:
1105+
sym_table = {}
11001106
self.sym_table_stack.append(self.sym_table)
11011107
self.sym_table = sym_table
11021108
for arg1 in arg.body:
@@ -1107,11 +1113,17 @@ async def ast_classdef(self, arg):
11071113
raise SyntaxError(f"{val.name()} statement outside loop")
11081114
self.sym_table = self.sym_table_stack.pop()
11091115

1116+
decorators = [await self.aeval(dec) for dec in arg.decorator_list]
11101117
sym_table["__init__evalfunc_wrap__"] = None
11111118
if "__init__" in sym_table:
11121119
sym_table["__init__evalfunc_wrap__"] = sym_table["__init__"]
11131120
del sym_table["__init__"]
1114-
sym_table_assign[arg.name].set(type(arg.name, tuple(bases), sym_table))
1121+
cls = metaclass(arg.name, tuple(bases), sym_table, **keywords)
1122+
if inspect.iscoroutine(cls):
1123+
cls = await cls
1124+
for dec_func in reversed(decorators):
1125+
cls = await self.call_func(dec_func, None, cls)
1126+
sym_table_assign[arg.name].set(cls)
11151127

11161128
async def ast_functiondef(self, arg, async_func=False):
11171129
"""Evaluate function definition."""
@@ -1488,7 +1500,11 @@ async def ast_augassign(self, arg):
14881500
await self.recurse_assign(arg.target, new_val)
14891501

14901502
async def ast_annassign(self, arg):
1491-
"""Execute type hint assignment statement (just ignore the type hint)."""
1503+
"""Execute type hint assignment statement and track __annotations__."""
1504+
if isinstance(arg.target, ast.Name):
1505+
annotations = self.sym_table.setdefault("__annotations__", {})
1506+
if arg.annotation:
1507+
annotations[arg.target.id] = await self.aeval(arg.annotation)
14921508
if arg.value is not None:
14931509
rhs = await self.aeval(arg.value)
14941510
await self.recurse_assign(arg.target, rhs)
@@ -1962,19 +1978,25 @@ async def call_func(self, func, func_name, *args, **kwargs):
19621978
if isinstance(func, (EvalFunc, EvalFuncVar)):
19631979
return await func.call(self, *args, **kwargs)
19641980
if inspect.isclass(func) and hasattr(func, "__init__evalfunc_wrap__"):
1965-
inst = func()
1981+
has_init_wrapper = getattr(func, "__init__evalfunc_wrap__") is not None
1982+
inst = func(*args, **kwargs) if not has_init_wrapper else func()
19661983
#
19671984
# we use weak references when we bind the method calls to the instance inst;
19681985
# otherwise these self references cause the object to not be deleted until
19691986
# it is later garbage collected
19701987
#
19711988
inst_weak = weakref.ref(inst)
19721989
for name in dir(inst):
1973-
value = getattr(inst, name)
1990+
try:
1991+
value = getattr(inst, name)
1992+
except AttributeError:
1993+
# same effect as hasattr (which also catches AttributeError)
1994+
# dir() may list names that aren't actually accessible attributes
1995+
continue
19741996
if type(value) is not EvalFuncVar:
19751997
continue
19761998
setattr(inst, name, EvalFuncVarClassInst(value.get_func(), value.get_ast_ctx(), inst_weak))
1977-
if getattr(func, "__init__evalfunc_wrap__") is not None:
1999+
if has_init_wrapper:
19782000
#
19792001
# since our __init__ function is async, call the renamed one
19802002
#

tests/test_unit_eval.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,68 @@
144144
["x: int = [10, 20]; x", [10, 20]],
145145
["Foo = type('Foo', (), {'x': 100}); Foo.x = 10; Foo.x", 10],
146146
["Foo = type('Foo', (), {'x': 100}); Foo.x += 10; Foo.x", 110],
147+
[
148+
"""
149+
from enum import IntEnum
150+
151+
class TestIntMode(IntEnum):
152+
VAL1 = 1
153+
VAL2 = 2
154+
VAL3 = 3
155+
[TestIntMode.VAL2 == 2, isinstance(TestIntMode.VAL3, IntEnum)]
156+
""",
157+
[True, True],
158+
],
159+
[
160+
"""
161+
from enum import StrEnum
162+
163+
class TestStrEnum(StrEnum):
164+
VAL1 = "val1"
165+
VAL2 = "val2"
166+
VAL3 = "val3"
167+
[TestStrEnum.VAL2 == "val2", isinstance(TestStrEnum.VAL3, StrEnum)]
168+
""",
169+
[True, True],
170+
],
171+
[
172+
"""
173+
from enum import Enum, EnumMeta
174+
175+
class Color(Enum):
176+
RED = 1
177+
BLUE = 2
178+
[type(Color) is EnumMeta, isinstance(Color.RED, Color), list(Color.__members__.keys())]
179+
""",
180+
[True, True, ["RED", "BLUE"]],
181+
],
182+
[
183+
"""
184+
from dataclasses import dataclass
185+
186+
@dataclass()
187+
class DT:
188+
name: str
189+
num: int = 32
190+
obj1 = DT(name="abc")
191+
obj2 = DT("xyz", 5)
192+
[obj1.name, obj1.num, obj2.name, obj2.num]
193+
""",
194+
["abc", 32, "xyz", 5],
195+
],
196+
[
197+
"""
198+
class Meta(type):
199+
def __new__(mcls, name, bases, ns, flag=False):
200+
ns["flag"] = flag
201+
return type.__new__(mcls, name, bases, ns)
202+
203+
class Foo(metaclass=Meta, flag=True):
204+
pass
205+
[Foo.flag, isinstance(Foo, Meta)]
206+
""",
207+
[True, True],
208+
],
147209
["Foo = [type('Foo', (), {'x': 100})]; Foo[0].x = 10; Foo[0].x", 10],
148210
["Foo = [type('Foo', (), {'x': [100, 101]})]; Foo[0].x[1] = 10; Foo[0].x", [100, 10]],
149211
["Foo = [type('Foo', (), {'x': [0, [[100, 101]]]})]; Foo[0].x[1][0][1] = 10; Foo[0].x[1]", [[100, 10]]],

0 commit comments

Comments
 (0)