|
29 | 29 | Method |
30 | 30 | ------ |
31 | 31 |
|
32 | | -AST, not regex. Every function that returns an EEVEE id is compiled and |
33 | | -*called* twice, against a stubbed ``bpy.app.version`` of (4, 5, 11) and |
34 | | -(5, 2, 1); every conditional expression that yields one is evaluated the |
35 | | -same way. Both forms — ternary and ``if``/``return`` — are handled by the |
| 32 | +AST, not regex. Every function that returns an EEVEE id is interpreted |
| 33 | +twice, against a stubbed ``bpy.app.version`` of (4, 5, 11) and (5, 2, 1); |
| 34 | +every conditional expression that yields one is evaluated the same way. The |
| 35 | +interpreter walks the AST and never runs the repo's code (no eval or exec). Both forms — ternary and ``if``/``return`` — are handled by the |
36 | 36 | same code path, because a regex over either one misreads the other. |
37 | 37 |
|
38 | 38 | A deliberately inverted mapping (``examples/swatch-grid`` witnesses the |
@@ -63,16 +63,6 @@ def canonical(version): |
63 | 63 | return "BLENDER_EEVEE" if version >= (5, 0, 0) else "BLENDER_EEVEE_NEXT" |
64 | 64 |
|
65 | 65 |
|
66 | | -class _App: |
67 | | - def __init__(self, version): |
68 | | - self.version = version |
69 | | - |
70 | | - |
71 | | -class _Bpy: |
72 | | - def __init__(self, version): |
73 | | - self.app = _App(version) |
74 | | - |
75 | | - |
76 | 66 | def _mentions_id(node): |
77 | 67 | for sub in ast.walk(node): |
78 | 68 | if isinstance(sub, ast.Constant) and sub.value in EEVEE_IDS: |
@@ -119,26 +109,85 @@ def walk(stmts): |
119 | 109 | return walk(fn.body) and bool(returns) and not fn.args.args |
120 | 110 |
|
121 | 111 |
|
122 | | -def _namespace(version): |
123 | | - return { |
124 | | - "bpy": _Bpy(version), |
125 | | - "IS_5X": version >= (5, 0, 0), |
126 | | - "IS_5_X": version >= (5, 0, 0), |
127 | | - "__builtins__": __builtins__, |
128 | | - } |
| 112 | +# Names a resolver may branch on, as functions of the stubbed version. |
| 113 | +_NAMES = { |
| 114 | + "IS_5X": lambda version: version >= (5, 0, 0), |
| 115 | + "IS_5_X": lambda version: version >= (5, 0, 0), |
| 116 | +} |
| 117 | +_COMPARE = { |
| 118 | + ast.Lt: lambda a, b: a < b, ast.LtE: lambda a, b: a <= b, |
| 119 | + ast.Gt: lambda a, b: a > b, ast.GtE: lambda a, b: a >= b, |
| 120 | + ast.Eq: lambda a, b: a == b, ast.NotEq: lambda a, b: a != b, |
| 121 | +} |
| 122 | + |
| 123 | + |
| 124 | +def _value(node, version): |
| 125 | + """Interpret the small expression language resolvers use. |
| 126 | +
|
| 127 | + Constants, tuples, ``bpy.app.version`` (indexed or sliced), the IS_5X |
| 128 | + flags, comparisons, and/or/not, and conditional expressions. Anything |
| 129 | + else raises ValueError, which the caller reports as "not resolvable". |
| 130 | + The repo's own code is walked, never executed, so the check needs no |
| 131 | + eval/exec (HOL's plugin scanner rejects dynamic execution, #510). |
| 132 | + """ |
| 133 | + if isinstance(node, ast.Constant): |
| 134 | + return node.value |
| 135 | + if isinstance(node, ast.Tuple): |
| 136 | + return tuple(_value(e, version) for e in node.elts) |
| 137 | + if isinstance(node, ast.Name) and node.id in _NAMES: |
| 138 | + return _NAMES[node.id](version) |
| 139 | + if (isinstance(node, ast.Attribute) and node.attr == "version" |
| 140 | + and isinstance(node.value, ast.Attribute) and node.value.attr == "app" |
| 141 | + and isinstance(node.value.value, ast.Name) and node.value.value.id == "bpy"): |
| 142 | + return version |
| 143 | + if isinstance(node, ast.Subscript): |
| 144 | + seq = _value(node.value, version) |
| 145 | + if isinstance(node.slice, ast.Slice): |
| 146 | + lo, hi, step = (None if p is None else _value(p, version) |
| 147 | + for p in (node.slice.lower, node.slice.upper, node.slice.step)) |
| 148 | + return seq[lo:hi:step] |
| 149 | + return seq[_value(node.slice, version)] |
| 150 | + if isinstance(node, ast.Compare): |
| 151 | + left = _value(node.left, version) |
| 152 | + for op, comp in zip(node.ops, node.comparators): |
| 153 | + right = _value(comp, version) |
| 154 | + if type(op) not in _COMPARE: |
| 155 | + raise ValueError(f"unsupported comparison {type(op).__name__}") |
| 156 | + if not _COMPARE[type(op)](left, right): |
| 157 | + return False |
| 158 | + left = right |
| 159 | + return True |
| 160 | + if isinstance(node, ast.BoolOp): |
| 161 | + vals = (_value(v, version) for v in node.values) |
| 162 | + return all(vals) if isinstance(node.op, ast.And) else any(vals) |
| 163 | + if isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.Not): |
| 164 | + return not _value(node.operand, version) |
| 165 | + if isinstance(node, ast.IfExp): |
| 166 | + branch = node.body if _value(node.test, version) else node.orelse |
| 167 | + return _value(branch, version) |
| 168 | + raise ValueError(f"unsupported expression {ast.unparse(node)!r}") |
| 169 | + |
| 170 | + |
| 171 | +def _run(stmts, version): |
| 172 | + """Interpret a resolver body (returns, version branches, docstrings).""" |
| 173 | + for st in stmts: |
| 174 | + if isinstance(st, ast.Return): |
| 175 | + return True, _value(st.value, version) |
| 176 | + if isinstance(st, ast.If): |
| 177 | + done, val = _run(st.body if _value(st.test, version) else st.orelse, version) |
| 178 | + if done: |
| 179 | + return True, val |
| 180 | + return False, None |
129 | 181 |
|
130 | 182 |
|
131 | 183 | def _evaluate(node, version): |
132 | 184 | """Return the id this node yields on *version*.""" |
133 | | - ns = _namespace(version) |
134 | 185 | if isinstance(node, ast.FunctionDef): |
135 | | - mod = ast.Module(body=[node], type_ignores=[]) |
136 | | - ast.fix_missing_locations(mod) |
137 | | - exec(compile(mod, "<engine-id>", "exec"), ns) |
138 | | - return ns[node.name]() |
139 | | - expr = ast.Expression(body=node) |
140 | | - ast.fix_missing_locations(expr) |
141 | | - return eval(compile(expr, "<engine-id>", "eval"), ns) |
| 186 | + done, val = _run(node.body, version) |
| 187 | + if not done: |
| 188 | + raise ValueError(f"{node.name}() falls off the end without returning") |
| 189 | + return val |
| 190 | + return _value(node, version) |
142 | 191 |
|
143 | 192 |
|
144 | 193 | def _exempt_reason(lines, lineno): |
|
0 commit comments