-
Notifications
You must be signed in to change notification settings - Fork 89
Expand file tree
/
Copy pathPythonCode.java
More file actions
225 lines (201 loc) · 8.65 KB
/
PythonCode.java
File metadata and controls
225 lines (201 loc) · 8.65 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
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:
* <pre><code>
* def cook(self):
* return Polygon.rect(self.x, self.y, self.width, self.height)
* </code></pre>
* <p/>
* 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".
* <p/>
* 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.
* <p/>
* 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.
* <p/>
* 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)
* <p/>
* 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));
}
}
}