package nodebox.function;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import nodebox.client.PythonUtils;
import nodebox.util.FileUtils;
import nodebox.util.LoadException;
import org.python.core.*;
import org.python.util.PythonInterpreter;
import java.io.File;
import java.io.IOException;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
import java.util.regex.Pattern;
import static com.google.common.base.Preconditions.checkArgument;
public class PythonLibrary extends FunctionLibrary {
private static final Pattern FILE_NAME_PATTERN = Pattern.compile("[a-z0-9_]+\\.py");
/**
* Given a file name, determines the namespace.
*
* @param fileName The file name. Should end in ".py".
* @return The namespace.
*/
public static String namespaceForFile(String fileName) {
checkArgument(fileName.endsWith(".py"), "The file name of a Python library needs to end in .py (not %s)", fileName);
checkArgument(fileName.trim().length() >= 4, "The file name can not be empty (was %s).", fileName);
File f = new File(fileName);
String baseName = f.getName();
checkArgument(FILE_NAME_PATTERN.matcher(baseName).matches(), "The file name can only contain lowercase letters, numbers and underscore (was %s).", fileName);
return baseName.substring(0, baseName.length() - 3);
}
/**
* Load the Python module.
* <p/>
* The namespace is determined automatically by using the file name.
*
* @param baseFile The file to which the path of this library is relative to.
* @param fileName The file name.
* @return The new Python library.
* @throws LoadException If the script could not be loaded.
* @see #namespaceForFile(String)
*/
public static PythonLibrary loadScript(File baseFile, String fileName) throws LoadException {
return loadScript(namespaceForFile(fileName), baseFile, fileName);
}
/**
* Load the Python module.
*
* @param namespace The name space in which the library resides.
* @param fileName The file name.
* @return The new Python library.
* @throws LoadException If the script could not be loaded.
*/
public static PythonLibrary loadScript(String namespace, String fileName) throws LoadException {
return loadScript(namespace, null, fileName);
}
/**
* Load the Python module.
*
* @param namespace The name space in which the library resides.
* @param baseFile The file to which the path of this library is relative to.
* @param fileName The file name.
* @return The new Python library.
* @throws LoadException If the script could not be loaded.
*/
public static PythonLibrary loadScript(String namespace, File baseFile, String fileName) throws LoadException {
File file;
if (baseFile != null) {
file = new File(baseFile, fileName);
} else {
file = new File(fileName);
}
if (!file.exists()) {
throw new LoadException(file, "Library does not exist.");
}
return new PythonLibrary(namespace, file, loadScript(file));
}
private static Future<ImmutableMap<String, Function>> loadScript(final File file) {
FutureTask<ImmutableMap<String, Function>> task = new FutureTask<ImmutableMap<String, Function>>(new Callable<ImmutableMap<String, Function>>() {
public ImmutableMap<String, Function> call() throws Exception {
// This creates a dependency between function and the client.
// However, we need to know the load paths before we can do anything, so this is necessary.
PythonUtils.initializePython();
Py.getSystemState().path.append(new PyString(file.getParentFile().getCanonicalPath()));
PythonInterpreter interpreter = new PythonInterpreter();
try {
interpreter.execfile(file.getCanonicalPath());
} catch (IOException e) {
throw new LoadException(file, e);
} catch (PyException e) {
throw new LoadException(file, e);
}
PyStringMap map = (PyStringMap) interpreter.getLocals();
ImmutableMap.Builder<String, Function> builder = ImmutableMap.builder();
for (Object key : map.keys()) {
Object o = map.get(Py.java2py(key));
if (o instanceof PyFunction) {
String name = (String) key;
Function f = new PythonFunction(name, (PyFunction) o);
builder.put(name, f);
}
}
return builder.build();
}
});
Thread t = new Thread(task);
t.start();
return task;
}
private final String namespace;
private final File file;
private Future<ImmutableMap<String, Function>> functionMap;
private PythonLibrary(String namespace, File file, Future<ImmutableMap<String, Function>> functionMap) {
this.namespace = namespace;
this.file = file;
this.functionMap = functionMap;
}
@Override
public String getLink(File baseFile) {
File parentFile = baseFile != null ? baseFile.getParentFile() : null;
return "python:" + FileUtils.getRelativeLink(file, parentFile);
}
public String getSimpleIdentifier() {
return file.getName();
}
public String getNamespace() {
return namespace;
}
public String getLanguage() {
return "python";
}
public File getFile() {
return file;
}
public ImmutableMap<String, Function> getFunctionMap() {
try {
return functionMap.get();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public Function getFunction(String name) {
return getFunctionMap().get(name);
}
public boolean hasFunction(String name) {
return getFunctionMap().containsKey(name);
}
/**
* Reloads the python module.
*/
@Override
public void reload() {
this.functionMap = loadScript(this.file);
// Because we don't want this to happen asynchronously, get the results of the functionMap immediately.
try {
this.functionMap.get();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private static final class PythonFunction implements Function {
private final String name;
private final PyFunction fn;
public PythonFunction(String name, PyFunction fn) {
this.name = name;
this.fn = fn;
}
public String getName() {
return name;
}
public Object invoke(Object... args) throws Exception {
PyObject[] pyArgs = new PyObject[args.length];
for (int i = 0; i < args.length; i++)
pyArgs[i] = Py.java2py(args[i]);
PyObject pyResult = fn.__call__(pyArgs);
if (pyResult == null)
return null;
// todo: number conversions should be handled higher up in the code, and not at the Jython level.
if (pyResult instanceof PyLong || pyResult instanceof PyInteger)
return pyResult.__tojava__(Long.class);
Object result = pyResult.__tojava__(Object.class);
if (result == Py.NoConversion)
throw new RuntimeException("Cannot convert Python object " + pyResult + " to java.");
return result;
}
public ImmutableList<Argument> getArguments() {
// todo: check if keeping a list of arguments makes sense in a python environment.
return ImmutableList.of();
}
}
}