diff --git a/src/org/python/jsr223/PyScriptEngine.java b/src/org/python/jsr223/PyScriptEngine.java index 1909321..1956fbc 100644 --- a/src/org/python/jsr223/PyScriptEngine.java +++ b/src/org/python/jsr223/PyScriptEngine.java @@ -21,12 +21,10 @@ public class PyScriptEngine extends AbstractScriptEngine implements Compilable, private final PythonInterpreter interp; private final ScriptEngineFactory factory; - private final PyModule module; PyScriptEngine(ScriptEngineFactory factory) { this.factory = factory; - interp = new PythonInterpreter(new PyScriptEngineScope(this, context)); - module = (PyModule)Py.getSystemState().modules.__finditem__("__main__"); + interp = PythonInterpreter.threadLocalStateInterpreter(new PyScriptEngineScope(this, context)); } public Object eval(String script, ScriptContext context) throws ScriptException { @@ -38,6 +36,7 @@ public class PyScriptEngine extends AbstractScriptEngine implements Compilable, interp.setIn(context.getReader()); interp.setOut(context.getWriter()); interp.setErr(context.getErrorWriter()); + interp.setLocals(new PyScriptEngineScope(this, context)); return interp.eval(code).__tojava__(Object.class); } catch (PyException pye) { throw scriptException(pye); @@ -93,6 +92,7 @@ public class PyScriptEngine extends AbstractScriptEngine implements Compilable, public Object invokeMethod(Object thiz, String name, Object... args) throws ScriptException, NoSuchMethodException { try { + interp.setLocals(new PyScriptEngineScope(this, context)); if (!(thiz instanceof PyObject)) { thiz = Py.java2py(thiz); } @@ -109,6 +109,7 @@ public class PyScriptEngine extends AbstractScriptEngine implements Compilable, public Object invokeFunction(String name, Object... args) throws ScriptException, NoSuchMethodException { try { + interp.setLocals(new PyScriptEngineScope(this, context)); PyObject function = interp.get(name); if (function == null) { throw new NoSuchMethodException(name); @@ -120,7 +121,7 @@ public class PyScriptEngine extends AbstractScriptEngine implements Compilable, } public T getInterface(Class clazz) { - return getInterface(module, clazz); + return getInterface(new PyModule("__jsr223__", interp.getLocals()), clazz); } public T getInterface(Object obj, Class clazz) { @@ -130,6 +131,7 @@ public class PyScriptEngine extends AbstractScriptEngine implements Compilable, if (clazz == null || !clazz.isInterface()) { throw new IllegalArgumentException("interface expected"); } + interp.setLocals(new PyScriptEngineScope(this, context)); final PyObject thiz = Py.java2py(obj); @SuppressWarnings("unchecked") T proxy = (T) Proxy.newProxyInstance( @@ -138,6 +140,7 @@ public class PyScriptEngine extends AbstractScriptEngine implements Compilable, new InvocationHandler() { public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { + interp.setLocals(new PyScriptEngineScope(PyScriptEngine.this, context)); PyObject pyMethod = thiz.__findattr__(method.getName()); if (pyMethod == null) throw new NoSuchMethodException(method.getName()); diff --git a/src/org/python/jsr223/PyScriptEngineScope.java b/src/org/python/jsr223/PyScriptEngineScope.java index 9362797..9a51420 100644 --- a/src/org/python/jsr223/PyScriptEngineScope.java +++ b/src/org/python/jsr223/PyScriptEngineScope.java @@ -13,6 +13,12 @@ import org.python.expose.ExposedType; import org.python.expose.ExposedGet; import org.python.expose.ExposedMethod; +/** + * JSR 223 does not map well to Jython's concept of "locals" and "globals". + * Instead, SimpleScriptContext provides ENGINE_SCOPE and GLOBAL_SCOPE, each + * with its own bindings. We adapt this multi-scope object for use as both + * a local and global dictionary. + */ @ExposedType(name = "scope", isBaseType = false) public final class PyScriptEngineScope extends PyObject { public static final PyType TYPE = PyType.fromClass(PyScriptEngineScope.class); @@ -38,15 +44,13 @@ public final class PyScriptEngineScope extends PyObject { @ExposedMethod public PyObject scope_keys() { PyList members = new PyList(); - synchronized (context) { - List scopes = context.getScopes(); - for (int scope : scopes) { - Bindings bindings = context.getBindings(scope); - if (bindings == null) - continue; - for (String key : bindings.keySet()) - members.append(new PyString(key)); - } + List scopes = context.getScopes(); + for (int scope : scopes) { + Bindings bindings = context.getBindings(scope); + if (bindings == null) + continue; + for (String key : bindings.keySet()) + members.append(new PyString(key)); } members.sort(); return members; @@ -63,12 +67,10 @@ public final class PyScriptEngineScope extends PyObject { } public PyObject __finditem__(String key) { - synchronized (context) { - int scope = context.getAttributesScope(key); - if (scope == -1) - return null; - return Py.java2py(context.getAttribute(key, scope)); - } + int scope = context.getAttributesScope(key); + if (scope == -1) + return null; + return Py.java2py(context.getAttribute(key, scope)); } @ExposedMethod @@ -77,12 +79,10 @@ public final class PyScriptEngineScope extends PyObject { } public void __setitem__(String key, PyObject value) { - synchronized (context) { - int scope = context.getAttributesScope(key); - if (scope == -1) - scope = ScriptContext.ENGINE_SCOPE; - context.setAttribute(key, value.__tojava__(Object.class), scope); - } + int scope = context.getAttributesScope(key); + if (scope == -1) + scope = ScriptContext.ENGINE_SCOPE; + context.setAttribute(key, value.__tojava__(Object.class), scope); } @ExposedMethod @@ -91,11 +91,9 @@ public final class PyScriptEngineScope extends PyObject { } public void __delitem__(String key) { - synchronized (context) { - int scope = context.getAttributesScope(key); - if (scope == -1) - throw Py.KeyError(key); - context.removeAttribute(key, scope); - } + int scope = context.getAttributesScope(key); + if (scope == -1) + throw Py.KeyError(key); + context.removeAttribute(key, scope); } } diff --git a/src/org/python/util/PythonInterpreter.java b/src/org/python/util/PythonInterpreter.java index 985ae8f..80e1e91 100644 --- a/src/org/python/util/PythonInterpreter.java +++ b/src/org/python/util/PythonInterpreter.java @@ -27,65 +27,83 @@ import org.python.core.PyFileReader; */ public class PythonInterpreter { - PyModule module; - + // These variables are the defaults if the interpreter uses thread-local state. protected PySystemState systemState; + PyObject globals; - PyObject locals; + protected ThreadLocal threadLocals; protected CompilerFlags cflags = new CompilerFlags(); /** - * Initializes the jython runtime. This should only be called once, and should be called before - * any other python objects are created (including a PythonInterpreter). + * Initializes the Jython runtime. This should only be called once, and should be called before + * any other Python objects are created (including a PythonInterpreter). * * @param preProperties * A set of properties. Typically System.getProperties() is used. * PreProperties override properties from the registry file. * @param postProperties * An other set of properties. Values like python.home, python.path and all other - * values from the registry files can be added to this property set. PostProperties + * values from the registry files can be added to this property set. postProperties * will override system properties and registry properties. * @param argv - * Command line argument. These values will assigned to sys.argv. + * Command line argument. These values will be assigned to sys.argv. */ public static void initialize(Properties preProperties, Properties postProperties, String[] argv) { PySystemState.initialize(preProperties, postProperties, argv); } /** - * Create a new Interpreter with an empty dictionary + * Create a new interpreter with an empty local dictionary */ public PythonInterpreter() { this(null, null); } /** - * Create a new interpreter with the given dictionary to use as its namespace + * Create a new interpreter with optionally settable per-thread, + * rather than shared, globals/locals + */ + public static PythonInterpreter threadLocalStateInterpreter(PyObject dict) { + return new PythonInterpreter(dict, null, true); + } + + /** + * Create a new interpreter with the given dictionary as a global/local namespace */ public PythonInterpreter(PyObject dict) { this(dict, null); } public PythonInterpreter(PyObject dict, PySystemState systemState) { + this(dict, systemState, false); + } + + protected PythonInterpreter(PyObject dict, PySystemState systemState, boolean useThreadLocalState) { if (dict == null) { dict = new PyStringMap(); } - if (systemState == null) { + globals = dict; + + if (systemState == null) systemState = Py.getSystemState(); - if (systemState == null) { - systemState = new PySystemState(); - } - } this.systemState = systemState; - setState(); - module = new PyModule("__main__", dict); - systemState.modules.__setitem__("__main__", module); - locals = dict; + setSystemState(); + + if (useThreadLocalState) { + threadLocals = new ThreadLocal(); + } else { + PyModule module = new PyModule("__main__", dict); + systemState.modules.__setitem__("__main__", module); + } } - protected void setState() { - Py.setSystemState(systemState); + public PySystemState getSystemState() { + return systemState; + } + + protected void setSystemState() { + Py.setSystemState(getSystemState()); } /** @@ -95,7 +113,7 @@ public class PythonInterpreter { * Python file-like object to use as input stream */ public void setIn(PyObject inStream) { - systemState.stdin = inStream; + getSystemState().stdin = inStream; } public void setIn(java.io.Reader inStream) { @@ -119,7 +137,7 @@ public class PythonInterpreter { * Python file-like object to use as output stream */ public void setOut(PyObject outStream) { - systemState.stdout = outStream; + getSystemState().stdout = outStream; } public void setOut(java.io.Writer outStream) { @@ -137,7 +155,7 @@ public class PythonInterpreter { } public void setErr(PyObject outStream) { - systemState.stderr = outStream; + getSystemState().stderr = outStream; } public void setErr(java.io.Writer outStream) { @@ -152,24 +170,24 @@ public class PythonInterpreter { * Evaluate a string as Python source and return the result */ public PyObject eval(String s) { - setState(); - return __builtin__.eval(new PyString(s), locals); + setSystemState(); + return __builtin__.eval(new PyString(s), getLocals()); } /** * Evaluate a Python code object and return the result */ public PyObject eval(PyObject code) { - setState(); - return __builtin__.eval(code, locals, locals); + setSystemState(); + return __builtin__.eval(code, getLocals()); } /** * Execute a string of Python source in the local namespace */ public void exec(String s) { - setState(); - Py.exec(Py.compile_flags(s, "", CompileMode.exec, cflags), locals, locals); + setSystemState(); + Py.exec(Py.compile_flags(s, "", CompileMode.exec, cflags), getLocals(), null); Py.flushLine(); } @@ -177,8 +195,8 @@ public class PythonInterpreter { * Execute a Python code object in the local namespace */ public void exec(PyObject code) { - setState(); - Py.exec(code, locals, locals); + setSystemState(); + Py.exec(code, getLocals(), null); Py.flushLine(); } @@ -186,7 +204,8 @@ public class PythonInterpreter { * Execute a file of Python source in the local namespace */ public void execfile(String filename) { - setState(); + PyObject locals = getLocals(); + setSystemState(); __builtin__.execfile_flags(filename, locals, locals, cflags); Py.flushLine(); } @@ -196,8 +215,8 @@ public class PythonInterpreter { } public void execfile(java.io.InputStream s, String name) { - setState(); - Py.runCode(Py.compile_flags(s, name, CompileMode.exec, cflags), locals, locals); + setSystemState(); + Py.runCode(Py.compile_flags(s, name, CompileMode.exec, cflags), getLocals(), null); Py.flushLine(); } @@ -219,17 +238,26 @@ public class PythonInterpreter { } public PyCode compile(Reader reader, String filename) { mod node = ParserFacade.parseExpressionOrModule(reader, filename, cflags); - setState(); + setSystemState(); return Py.compile_flags(node, filename, CompileMode.eval, cflags); } public PyObject getLocals() { - return locals; + if (threadLocals == null) + return globals; + + PyObject locals = threadLocals.get(); + if (locals != null) + return locals; + return globals; } public void setLocals(PyObject d) { - locals = d; + if (threadLocals == null) + globals = d; + else + threadLocals.set(d); } /** @@ -242,7 +270,7 @@ public class PythonInterpreter { * appropriate Python object. */ public void set(String name, Object value) { - locals.__setitem__(name.intern(), Py.java2py(value)); + getLocals().__setitem__(name.intern(), Py.java2py(value)); } /** @@ -254,7 +282,7 @@ public class PythonInterpreter { * the value to set the variable to */ public void set(String name, PyObject value) { - locals.__setitem__(name.intern(), value); + getLocals().__setitem__(name.intern(), value); } /** @@ -265,12 +293,14 @@ public class PythonInterpreter { * @return the value of the variable, or null if that name isn't assigned */ public PyObject get(String name) { - return locals.__finditem__(name.intern()); + return getLocals().__finditem__(name.intern()); } /** - * Get the value of a variable in the local namespace Value will be returned as an instance of - * the given Java class. interp.get("foo", Object.class) will return the most + * Get the value of a variable in the local namespace. + * + * The value will be returned as an instance of the given Java class. + * interp.get("foo", Object.class) will return the most * appropriate generic Java object. * * @param name @@ -280,7 +310,7 @@ public class PythonInterpreter { * @return the value of the variable as the given class, or null if that name isn't assigned */ public T get(String name, Class javaclass) { - PyObject val = locals.__finditem__(name.intern()); + PyObject val = getLocals().__finditem__(name.intern()); if (val == null) { return null; } @@ -288,7 +318,8 @@ public class PythonInterpreter { } public void cleanup() { - systemState.callExitFunc(); + setSystemState(); + Py.getSystemState().callExitFunc(); try { Py.getSystemState().stdout.invoke("flush"); } catch (PyException pye) { diff --git a/src/org/python/util/jython.java b/src/org/python/util/jython.java index c73dc35..7400753 100644 --- a/src/org/python/util/jython.java +++ b/src/org/python/util/jython.java @@ -221,14 +221,14 @@ public class jython { } } else if (opts.filename.equals("-")) { try { - interp.locals.__setitem__(new PyString("__file__"), new PyString("")); + interp.globals.__setitem__(new PyString("__file__"), new PyString("")); interp.execfile(System.in, ""); } catch (Throwable t) { Py.printException(t); } } else { try { - interp.locals.__setitem__(new PyString("__file__"), + interp.globals.__setitem__(new PyString("__file__"), new PyString(opts.filename)); FileInputStream file; diff --git a/tests/java/org/python/jsr223/ScriptEngineTest.java b/tests/java/org/python/jsr223/ScriptEngineTest.java index 69251a6..5af7e4a 100644 --- a/tests/java/org/python/jsr223/ScriptEngineTest.java +++ b/tests/java/org/python/jsr223/ScriptEngineTest.java @@ -2,6 +2,7 @@ package org.python.jsr223; import java.io.IOException; import java.io.StringReader; +import javax.script.Bindings; import javax.script.Compilable; import javax.script.CompiledScript; import javax.script.Invocable; @@ -102,6 +103,46 @@ public class ScriptEngineTest extends TestCase { assertNull(pythonEngine.get("x")); } + class ThreadLocalBindingsTest implements Runnable { + ScriptEngine engine; + Object x; + Throwable exception; + + public ThreadLocalBindingsTest(ScriptEngine engine) { + this.engine = engine; + } + + public void run() { + try { + Bindings bindings = engine.createBindings(); + assertNull(engine.eval("try: a\nexcept NameError: pass\nelse: raise Exception('a is defined', a)", bindings)); + bindings.put("x", -7); + x = engine.eval("x", bindings); + } catch (Throwable e) { + e.printStackTrace(); + exception = e; + } + } + } + + public void testThreadLocalBindings() throws ScriptException, InterruptedException { + ScriptEngineManager manager = new ScriptEngineManager(); + ScriptEngine pythonEngine = manager.getEngineByName("python"); + + pythonEngine.put("a", 42); + pythonEngine.put("x", 15); + + ThreadLocalBindingsTest test = new ThreadLocalBindingsTest(pythonEngine); + Thread thread = new Thread(test); + thread.run(); + thread.join(); + assertNull(test.exception); + assertEquals(Integer.valueOf(-7), test.x); + assertEquals(Integer.valueOf(15), pythonEngine.get("x")); + assertNull(pythonEngine.eval("del x")); + assertNull(pythonEngine.get("x")); + } + public void testInvoke() throws ScriptException, NoSuchMethodException { ScriptEngineManager manager = new ScriptEngineManager(); ScriptEngine pythonEngine = manager.getEngineByName("python");