package nodebox.node; import nodebox.graphics.CanvasContext; import org.python.core.*; import org.python.util.PythonInterpreter; import java.io.File; import java.io.PrintStream; import java.util.Locale; /** * Python source code is in this form: *
* def cook(self):
* return Polygon.rect(self.x, self.y, self.width, self.height)
*
*
* Self is a reference that points to a node access proxy that allows direct access to the node's parameter values.
* You can access the node object using "self.node".
*
* When creating a PythonCode object, it is executed immediately to extract the "cook" function. Source code
* that has no or invalid "cook" function will throw an IllegalArgumentException.
*
* The cook method on this class executes the Python "cook" function with the self reference. It also sets a number
* of global parameters based on the ProcessingContext.
*/
public class PythonCode implements NodeCode {
public static final String TYPE = "python";
private String source;
private PyCode code;
private PyDictionary namespace;
private PyFunction cookFunction;
private CanvasContext ctx;
static {
// Monkey patch to facilitate access to a node's prototype cook code.
PythonInterpreter interpreter = new PythonInterpreter();
interpreter.exec("from nodebox.node import Node, PythonCode\n" +
"_cook = Node.cook\n" +
"def cook(self, *args):\n" +
" if len(args) == 1 and isinstance(args[0], PythonCode.SelfWrapper):\n" +
" wrapper = args[0]\n" +
" return self.asCode(\"_code\").cook(wrapper.node, wrapper.context)\n" +
" else:\n" +
" return _cook(self, *args)\n" +
"Node.cook = cook");
}
public PythonCode(String source) {
this.source = source;
namespace = new PyDictionary();
}
private void preCook() {
// The namespace will remain bound to the interpreter.
// Changes to this dictionary will affect the namespace of the interpreter.
PythonInterpreter interpreter = new PythonInterpreter(namespace);
// Immediately run the code to extract the cook(self) method.
interpreter.exec("from nodebox1.graphics import Context\n" +
"_g = globals()\n" +
"_ctx = Context(ns=_g)\n" +
"for n in dir(_ctx):\n" +
" _g[n] = getattr(_ctx, n)");
if (code == null)
code = new PythonInterpreter().compile(source);
interpreter.exec(code);
ctx = (CanvasContext) interpreter.get("_ctx").__tojava__(CanvasContext.class);
try {
cookFunction = (PyFunction) interpreter.get("cook");
//if (cookFunction == null)
// throw new RuntimeException("Source code does not contain a function \"cook(self)\".");
} catch (ClassCastException e) {
throw new RuntimeException("Attribute \"cook\" in source code is not a function.");
}
// We cannot check if the function takes only one (required) argument.
// If the function has more arguments, this will throw an error when cooking.
}
public Object cook(Node node, ProcessingContext context) throws RuntimeException {
// Reassign the output and error streams.
PrintStream oldOutStream = System.out;
PrintStream oldErrStream = System.err;
System.setOut(context.getOutputStream());
System.setErr(context.getErrorStream());
PySystemState ss = Py.getSystemState();
PyObject oldStdout = ss.stdout;
PyObject oldStderr = ss.stderr;
ss.stdout = Py.java2py(context.getOutputStream());
ss.stderr = Py.java2py(context.getErrorStream());
// Set the current working directory.
File libraryFile = null;
if (node != null && node.getLibrary() != null) {
libraryFile = node.getLibrary().getFile();
}
String originalWorkingDir = null;
if (libraryFile != null) {
originalWorkingDir = Py.getSystemState().getCurrentWorkingDir();
Py.getSystemState().setCurrentWorkingDir(libraryFile.getParent());
}
// Run the Python function.
PyObject pyResult = null;
try {
PyObject self;
if (node == null) {
self = Py.None;
} else {
self = new SelfWrapper(node, context);
}
// Add globals into the function namespace.
namespace.put("context", context);
namespace.put("node", self);
namespace.put("FRAME", context.getFrame());
if (ctx != null)
ctx.getCanvas().clear();
if (cookFunction == null) preCook();
if (cookFunction != null) {
pyResult = cookFunction.__call__(self);
}
} finally {
// Reset the output streams.
System.setOut(oldOutStream);
System.setErr(oldErrStream);
ss.stdout = oldStdout;
ss.stderr = oldStderr;
}
// Unwrap the result.
Object result;
if (pyResult != null) {
result = pyResult.__tojava__(Object.class);
if (result == Py.NoConversion) {
throw new RuntimeException("Cannot convert Python object " + pyResult + " to java.");
}
} else {
CanvasContext g = null;
try {
g = (CanvasContext) namespace.get("_ctx");
result = g.getCanvas().asGeometry();
} catch (ClassCastException e) {
result = null;
}
}
// Reset the current working directory.
if (originalWorkingDir != null) {
Py.getSystemState().setCurrentWorkingDir(originalWorkingDir);
}
return result;
}
public String getSource() {
return source;
}
public String getType() {
return TYPE;
}
/**
* The self wrapper allows easy access to parameter values from the node.
* Instead of doing node.asString("someparameter"), you can use self.someparameter.
* You can also access the node itself by querying self.node.
*
* The code here looks similar to the NodeAccessProxy, but it is not the same. Specifically, we don't give users
* easy access to parameters/ports on other nodes. In principle, all the behaviour of the
* node should be self-contained, which means that everything the node needs to operate is set in its parameters
* and ports. Therefore it should not have access to other nodes. For nodes that implement special functionality
* (such as the copy node), they can still access everything through the node reference.
*/
public class SelfWrapper extends PyObject {
private Node node;
private ProcessingContext context;
public SelfWrapper(Node node, ProcessingContext context) {
this.node = node;
this.context = context;
}
@Override
public PyObject __findattr_ex__(String name) {
if ("node".equals(name)) return Py.java2py(node);
if ("context".equals(name)) return Py.java2py(context);
Parameter p = node.getParameter(name);
if (p == null) {
Port port = node.getPort(name);
if (port == null) {
// This will throw an error that we explicitly do not catch.
noParameterOrPortError(name);
throw new AssertionError("noParameterOrPortError method should have thrown an error.");
} else {
if (port.getCardinality() == Port.Cardinality.SINGLE) {
return Py.java2py(port.getValue());
} else {
return Py.java2py(port.getValues());
}
}
} else {
return Py.java2py(p.getValue());
}
}
/**
* This method throws an error that will be shown to users referring to non-existant parameters.
* (e.g. self.inventedParameter)
*
* We could use the original noAttributeError, but that results in an ugly object name (a reference
* to this proxy class). We'd much rather refer to the node identifier and parameter/port.
*
* @param name the name of the parameter
*/
public void noParameterOrPortError(String name) {
throw Py.AttributeError(String.format(Locale.US, "Node '%.50s' has no parameter or port '%.400s'",
node.getIdentifier(), name));
}
}
}