Reputation: 164
I'm working through the Advent of Code 2017 problems for fun, and a few of them require that you interpret and execute pseudo-assembly programs. I want to write a reuse module that provides the necessary infrastructure for this. Specifically, I want to provide sandboxed exec
and eval
functions, and a function that resolves a variable in the interpretation namespace. I read about metaclasses and started framing one up, but I'm getting stuck on the implementation details. I envision being able to do this:
class InterpreterMixinMeta(type):
"""see below for my attempt"""
pass
class InterpreterMixin(metaclass=InterpreterMixinMeta):
pass
class Program(InterpreterMixin):
"""does the actual work of running a pseudo-assembly program"""
pass
Here's my initial attempt at InterpreterMixinMeta
. How do I create properties and methods on the object that will use the mixin? Properties like a unique copy of the locals dict for execs
and evals
, methods like val()
.
class InterpreterMixinMeta(type):
"""
A constructor for InterpreterMixin. Creates a new mixin class that
exposes functions needed for interpreting pseudo-assembly programs:
`execs`, `evals`, and `val`
"""
@staticmethod
def init_execs(exc_locals: dict):
"""Creates a persistent sandbox environment for calls to `exec`"""
sbox_globals = {'__builtins__': None}
def sbox_exec(source: str):
return exec(source, sbox_globals, exc_locals)
return sbox_exec
@staticmethod
def init_evals(exc_locals: dict):
"""Creates a persistent sandbox environment for calls to `eval`"""
sbox_globals = {'__builtins__': None}
def sbox_eval(expr: str):
return eval(expr, sbox_globals, exc_locals)
return sbox_eval
def val(self, x: str) -> str:
"""Returns the integer value of the input, formatted as a string"""
if x.isdigit():
return x
else:
return str(self.evals(x))
def __new__(mcs, name, bases, nspace):
# how do I let each object define a myLocals dict?
nspace['execs'] = InterpreterMixinMeta.init_execs(myLocals)
return super().__new__(mcs, name, bases, nspace)
Upvotes: 1
Views: 183
Reputation: 164
I solved this problem without using a metaclass. I first had to step back and rethink my object model. Ultimately, the roles and relationships I want to define are:
Each executor (instance) needs to initialize itself with a sandbox for the exec
and eval
functions. That's not what metaclasses are for: metaclasses are for initializing class objects...not class instance objects. To intialize a class instance object, I need to write a class. That's it: just a class.
So I wrote an Executor that initializes executor objects with their own locals
and globals
dicts. It also needs to provide functions that always use these dicts when called. That's what closures are for. And my approach to creating closures in each instance object was to monkey patch them in after initializing the object.
Here's the resultant Executor class:
from types import MethodType
class Executor:
"""Is a sandboxed executor and evaluator of Python statements."""
def __init__(self, sbox_globals: dict=None, sbox_locals: dict=None):
# Set up execution context for this instance
if sbox_globals is not None:
sbox_globals.setdefault('__builtins__', None)
self.sbox_globals = sbox_globals
else:
self.sbox_globals = {'__builtins__': None}
self.sbox_locals = {} if sbox_locals is None else sbox_locals
# Monkey patch closures onto this instance
def sbox_exec(self, source: str):
"""A persistent sandbox environment for calls to `exec`"""
return exec(source, self.sbox_globals, self.sbox_locals)
def sbox_eval(self, expr: str):
"""A persistent sandbox environment for calls to `eval`"""
return eval(expr, self.sbox_globals, self.sbox_locals)
self.execs = MethodType(sbox_exec, self)
self.evals = MethodType(sbox_eval, self)
Armed with this tool, I wrote a class that implements the Interpreter and Program roles at once, calling it Interpreter
. An interpreter (instance) composes an executor (instance) and implements the iterator protocol. It looks something like this:
from collections.abc import Iterator
class Interpreter(Iterator):
"""
Is a translator from input grammar to Python grammar. Is also an iterator
that executes one statement per call.
Has an Executor and uses it when called in iteration.
"""
def __init__(self, instruction_set: list):
self.exc = Executor(sbox_locals={})
# iterator protocol
self.instrs = instruction_set
self.i = 0
def __iter__(self):
return super().__iter__()
def __next__(self):
if 0 <= self.i < len(self.instrs):
instr = self.instrs[self.i][0]
param = self.instrs[self.i][1]
if instr == 'foo':
cmd = f'py_foo({param})'
self.exc.execs(cmd)
ret = f'{self.i}: ' + cmd
elif instr == 'bar':
cmd = f'py_bar({param})'
self.exc.execs(cmd)
ret = f'{self.i}: ' + cmd
else:
ret = f'{self.i}: {instr} no-op'
self.i += 1
return ret
else:
raise StopIteration
Upvotes: 1