"
This article is part of in the series
Last Updated: Wednesday 29th December 2021

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.

About The Author