There's a few things you can do with an interpreter in your app. One of the most interesting is giving your users the ability to script your app at runtime, like GIMP and Scribus do, but it can also be used to build enhanced Python shells, like IPython.
First things first: Start by importing the InteractiveConsole
class from the Standard Library's code
module, then subclassing it so you can add some hooks. There was no real change to the code
module between Python 2 and 3, so this article is valid for both.
[python]
from code import InteractiveConsole
class Console(InteractiveConsole):
def __init__(*args): InteractiveConsole.__init__(*args)
[/python]
The code so far is just boilerplate; it creates a class named Console
that simply subclasses InteractiveConsole
.
The following code shows how the class works. Line 4 calls the runcode
method, which is inherited from InteractiveConsole
. The runcode
method takes a string of source code and executes it inside the console, in this case, assigning 1
to a
, then printing it.
[python]
a = 0
code = 'a = 1; print(a)'
console = Console()
console.runcode(code) # prints 1
print(a) # prints 0
[/python]
Line 5 prints 0
as the console has its own namespace. Note that the console
object is a regular, runtime object; it runs code in the same thread and process as the code that initialises it, so a call to runcode
will ordinarily block.
Python's code
module also provides an InteractiveInterpreter
class, which will automatically evaluate expressions, just like Python Interactive Mode, but it is more complex to use with multiline input. InteractiveConsole.runcode
accepts any arbitrary chunk of valid Python. Generally, you should use the InteractiveInterpreter
class when you need to work with a terminal, and InteractiveConsole
when your input will be complete blocks of Python, typically as files or as input strings from a graphical user interface.
Processing User Input
You often want process the user's input, maybe to transcompile a syntax extension, like IPython Magic, or do more complex macros.
Add a new static method to Console
named preprocess
that just accepts a string and returns it. Add another new method named enter
that takes the user's input, runs it through preprocess
, then passes it to runcode
. Doing it this way makes it easy to redefine the processor, either by subclassing Console
or by simply assigning a new callable to an instance's preprocess
attribute at runtime.
[python]
class Console(InteractiveConsole):
def __init__(*args): InteractiveConsole.__init__(*args)
def enter(self, source):
source = self.preprocess(source)
self.runcode(source)
@staticmethod
def preprocess(source): return source
console = Console()
console.preprocess = lambda source: source[4:]
console.enter('>>> print(1)')
[/python]
Prime the Namespace
The InteractiveConsole
class constructor takes an optional argument, a dictionary which is used to prime the console's namespace when it's created.
[python]
names = {'a': 1, 'b': 2}
console = Console(names) # prime the console
console.runcode('print(a+b)') # prints 3
[/python]
Passing in objects when you create a new Console
instance allows you to put any objects in the namespace the user may need for scripting your app. Critically, these can be references to particular runtime instances of objects, not just class and function definitions from library imports.
You now have the hooks you need to flesh out a console. Adding a spawn
method that calls enter
in a new thread, allows you to have inputs block or run in parallel. Extra points for adding a preprocessor that lets you write blocking and non-blocking inputs.
Access the Namespace
To access the console's namespace once it's been created, you can reference its locals
attribute.
[python]
console.locals['a'] = 1
console.runcode('print(a)') # prints 1
console.runcode('a = 2')
print(console.locals['a']) # prints 2
[/python]
Because this is a bit ugly, you can pass an empty module object into the console, keeping a reference to it in the outer space, then use the module to share objects.
The Standard Library's imp
module provides a new_module
function that lets us create a new module object without effecting sys.modules
(or needing an actual file). You need to pass the new module's name in to the function, and you get the empty module back.
[python]
from imp import new_module
superspace = new_module('superspace')
[/python]
You can now pass the superspace
module into the the console's namespace when it's created. Calling it superspace
inside the console as well just makes it more obvious that they're the same object, but you may use different names.
[python]
console = Console({'superspace': superspace})
[/python]
Now superspace
is a single, empty module object, that's referenced by that name in both namespaces.
[python]
superspace.a = 1
console.enter('print(superspace.a)') # prints 1
console.enter('superspace.a = 2')
print(superspace.a) # prints 2
[/python]
Rounding Up
It'd make sense to bind the shared module to the console instance, so each console instance has its own one. The __init__
method will need extending to handle it's args a bit more directly, so it's still able to accept an optional namespace dict.
It'd also be nice to pass a hook to the preprocessor into the console's namespace so the user can bind their own processors to it. For simplicity here, the following example just blatantly passes a reference to self
into its own namespace as console
~ not because it's meta, just because it's easier to read the code.
[python]
from code import InteractiveConsole
from imp import new_module
class Console(InteractiveConsole):
def __init__(self, names=None):
names = names or {}
names['console'] = self
InteractiveConsole.__init__(self, names)
self.superspace = new_module('superspace')
def enter(self, source):
source = self.preprocess(source)
self.runcode(source)
@staticmethod
def preprocess(source):
return source
console = Console()
[/python]
If you ran that code, there's now a global named console
in both the outer space and inside the console itself that reference the same thing, so console.superspace
is the same empty module in both.
In practice, if you're allowing users to script your app, you'll want to write a wrapper around the runtime objects you'd like to expose, so the user has a clean API they can hack on without crashing things. You'd then pass those wrapper objects into the console.