Blog

March 18, 2011
Categories:

adding a module to skulpt

Now that I’m back from traveling in South America I’ve started on a project to add a turtle graphics module to skulpt.  Skulpt is a javascript implementation of Python written by Scott Graham, and is available on skulpt.googlecode.com.  Its a very nice project because it allows you to embed a Python interpreter right in your web page.  I’ve written about how to do this in a previous post.
The skulpt interpreter works like a charm, but it does not yet have any of the Python standard library modules implemented.  Scott has been working with wrapping some of Googles closure libraries, and has a webgl module too.  But none of your regular Python friends are available, things like math, random, turtle, etc need to be implemented.  For the most part these things are already available in javascript so the work is in creating the module and then wrapping the underlying Javascript inside the standard Python API.
Where to start?  There’s not a lot of documentation provided for skulpt, so I’m hoping this post will be enough to help others get going a bit quicker.  Scott was very helpful in responding to all my emails so this is not a criticism of him, rather I’m hoping this will save him the effort the next time someone wants to extend skulpt.  So, here’s the deal.  skulpt relies on two javascript files the first is skulpt.js  and builtin.js  A very minimal installation only uses skulpt.js, whereas if you want to use any modules they are in builtin.js.  Looking around the distribution you will not immediately find skulpt.js because you need to build it. You get a sculpt.js file by using the m script that comes with the distribution.  running m –help will give you the full list of commands, but the two that you probably most care about are m dist and m docbi The dist command builds both skulpt.js and builtin.js  docbi builds builtin.js and puts a new copy of it in the doc/static directory.
Lets begin with a quick tour of the source tree:

  • src - contains the implementation of the Python interpreter
  • src/lib - has the module implementations of webgl and goog.  This is where turtle will live and any other modules I implement along the way.
  • doc - This directory contains a google app engine application and is what you see on skulpt.org There are a couple of important files to check out in here.  One of them is doc/static/env/editor.js  This is the code that ties together the interactive editor on the home page with the skulpt interpreter and the codemirror editor.  If you know how to build a google app engine app then this directory makes sense.  One thing about the home page is that it is not set up to use any of the modules.  The modules are used in the more advanced ide, which you can find in doc/ide/static.  I’m going to tell you how to add modules to the simpler editor later in this article.
  • test - this directory contains a bunch of files for testing the implementation in a batch mode.  These tests are run whenever you run m dist, or m test.
  • dist - This directory gets created and populated when you run the m dist command.  It contains the built and compressed versions of skulpt.js and builtin.js
To illustrate how to make use of modules, here’s an extended version of my earlier hello world style example.

<html>
<head>
<script src=“skulpt.js” type=“text/javascript”></script>
<script src=“builtin.js” type=“text/javascript”></script>

</head>

<body>
<script type=“text/javascript”>
function outf(text) {
var mypre = document.getElementById(“output”);
mypre.innerHTML = mypre.innerHTML + text;
}

function builtinRead(x)
{
if (Sk.builtinFiles === undefined || Sk.builtinFiles[“files”][x] === undefined)
throw “File not found: ‘” + x + “’”;
return Sk.builtinFiles[“files”][x];
}

function runit() {
var prog = document.getElementById(“yourcode”).value;
var mypre = document.getElementById(“output”);
mypre.innerHTML = “;
Sk.configure({output:outf,
read: builtinRead
});
try {
Sk.importMainWithBody(”<stdin>”,false,prog);
} catch (e) {
alert(e);
}
}
</script>
<h3>Try This</h3>
<form>
<textarea edit_id=“eta_5” id=“yourcode”>
print “Hello World”
</textarea>
<button onclick=“runit()” type=“button”>Run</button>
</form>

<pre id=“output”></pre>

</body>
</html>

There are some important differences between this version, and the version and the non-module version.  First off, the call to Sk.configure contains another key value pair which sets up a specialized read function.  This is the function that is responsible for returning your module out of the large array of files that are contained in the builtin.js file.  You will see that all of the modules are contained in this one file, stored in a big JSON structure.  The extra key value pair is:
read: builtinRead
The read function is just for loading modules and is called when you do an import statement of some kind.  In this case the function accesses the variable builtinFiles which is created from the builtin.js file.  The other difference, of course, is that you have to include builtin.js in your html file.  Note that builtin.js must be included after skulpt.js
Now as far as the module itself goes, the easiest thing to do is to start your module in the src/lib directory.  This way it will automatically get built and included in builtin.js.  If you don’t put it there then you are going to have to modify the m script, specifically the docbi function in the m script to include your module.  Suppose that you want to have a module called bnm.test  Here’s what you have to do.  First, you need to make a bnm directory under lib.  In this directory you will need to have either init.py or init.js or bnm.js to stand in for the bnm module.  There doesn’t need to be anything in the file as long as it exists.  This is just like CPython by the way.  Then to make a test module you can either make a test directory and put all your javascript code in init.js or you can simply create a test.js file in the bnm directory.  Lets look at the test module.
var $builtinmodule = function(name)
{
var mod = {};
var myfact = function(n) {
if(n < 1) {
return 1;
} else {
return n * myfact(n-1);
}
}
mod.fact = new Sk.builtin.func(function(a) {
return myfact(a);
});

mod.Stack = Sk.misceval.buildClass(mod, function($gbl, $loc) {
$loc.init = new Sk.builtin.func(function(self) {
self.stack = [];
});

$loc.push = new Sk.builtin.func(function(self,x) {
self.stack.push(x);
});
$loc.pop = new Sk.builtin.func(function(self) {
return self.stack.pop();
});
},
‘Stack’, []);


return mod;
}

All modules start out with the $var builtinmodule = statement.
This test module exposes a single method to the outside world, called fact, There are a couple of key functions for building up a module.  The Sk.builtin.func   call for adding functions to your module, and the Sk.misceval.buildClass method.  This test module defines a simple factorial function called fact, and a class called stack.  Here’s a simple Python program that exercises the module:
import bnm.test
print ‘starting’
print bnm.test.fact(10)
x = bnm.test.Stack()
x.push(1)
x.push(2)
print x.pop()
print ‘done’

Its not obvious, but the buildClass method takes four parameters:  globals, func, name, bases
It seems that you always pass the mod object itself as the globals parameter, the func parameter is a function that represents the class object, the Name is the external name of the class, and bases presumably would be if the class is inheriting from another class.
The Sk.builtin.func method creates a function.  For module creation we typically only have to worry about the one parameter, func, which is the javascript implementation of our Python function.  The method can also take a globals object and two closure objects.  Look at the comments in function.js if you want more explanation of how the builtin.func method works.
Well, I think this should be enough to get you going.  Its worth repeating, if you made it this far, don’t forget to call m docbi or m dist after you make changes in your module, its easy to get into the mode of thinking that the new javascript is automatically loaded.  But builtin.js is not automatically rebuilt!
I’ll consider this post a work in progress, please leave a comment if something is unclear or you would like something explained in more detail.