Skip to content

Commit 848558c

Browse files
gselzerclaude
andcommitted
Declare a live interpreter's variables by their values
Following scijava-code-lsp's ScriptDialect.header Variables: imported Java classes as the classes themselves, modules imported again (under their own names, e.g. "import ntpath as osp"), functions as functions, and other Python values as instances of their builtin types. Names Python reserves are skipped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent f692d17 commit 848558c

2 files changed

Lines changed: 177 additions & 13 deletions

File tree

‎src/main/java/org/scijava/plugins/scripting/jython/completion/JythonDialect.java‎

Lines changed: 114 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,21 @@
2828
*/
2929
package org.scijava.plugins.scripting.jython.completion;
3030

31+
import java.lang.reflect.Modifier;
3132
import java.math.BigInteger;
3233
import java.util.ArrayList;
34+
import java.util.Arrays;
3335
import java.util.Collection;
36+
import java.util.HashSet;
3437
import java.util.List;
3538
import java.util.Map;
3639
import java.util.Set;
3740
import java.util.TreeSet;
3841

42+
import org.python.core.PyFunction;
43+
import org.python.core.PyJavaType;
44+
import org.python.core.PyModule;
45+
import org.python.core.PyObject;
3946
import org.scijava.code.lsp.ScriptDialect;
4047
import org.scijava.plugin.Plugin;
4148
import org.scijava.plugins.scripting.jython.JythonScriptLanguage;
@@ -53,12 +60,26 @@
5360
* from stubs (see scijava-code-lsp-jython) read: e.g.
5461
* {@code import net.imglib2.img; image = net.imglib2.img.Img(); name = str()}.
5562
* </p>
63+
* <p>
64+
* A live interpreter's variables are declared by their values, where those
65+
* say more: its imported Java classes as the classes themselves, its modules
66+
* imported again, its functions as functions, and its other Python values as
67+
* instances of their builtin types.
68+
* </p>
5669
*
5770
* @author Gabriel Selzer
5871
*/
5972
@Plugin(type = ScriptDialect.class, name = "Jython")
6073
public class JythonDialect implements ScriptDialect {
6174

75+
/** Names Python reserves, which cannot be declared. */
76+
private static final Set<String> KEYWORDS = new HashSet<>(Arrays.asList(
77+
"False", "None", "True", "and", "as", "assert", "async", "await", "break",
78+
"class", "continue", "def", "del", "elif", "else", "except", "exec",
79+
"finally", "for", "from", "global", "if", "import", "in", "is", "lambda",
80+
"nonlocal", "not", "or", "pass", "print", "raise", "return", "try",
81+
"while", "with", "yield"));
82+
6283
@Override
6384
public boolean supports(final ScriptLanguage language) {
6485
return language instanceof JythonScriptLanguage;
@@ -70,14 +91,14 @@ public String languageId() {
7091
}
7192

7293
@Override
73-
public Header header(final Map<String, Class<?>> parameters) {
74-
// Declare each parameter as an instance of its type, for type reasoning
94+
public Header header(final Map<String, Variable> variables) {
95+
// Declare each variable, for type reasoning
7596
final List<String> types = new ArrayList<>();
7697
final List<String> declarations = new ArrayList<>();
77-
parameters.forEach((name, type) -> {
78-
final String t = parameterType(type);
79-
types.add(t);
80-
declarations.add(name + " = " + t.replace('$', '.') + "()");
98+
variables.forEach((name, v) -> {
99+
// NB: Names Python reserves cannot be declared.
100+
if (!KEYWORDS.contains(name)) declarations.add(declaration(name, v,
101+
types));
81102
});
82103
// And import the packages of those types, before the declarations
83104
final List<String> code = new ArrayList<>();
@@ -92,6 +113,93 @@ public Header header(final Map<String, Class<?>> parameters) {
92113
return new Header(String.join("; ", code), hidden);
93114
}
94115

116+
/**
117+
* Declares a variable: by its value, if known (e.g. a live interpreter's
118+
* imported Java class, as the class itself), or else as an instance of its
119+
* type.
120+
*
121+
* @param types Where to note the types the declaration refers to (see
122+
* {@link #packages}).
123+
*/
124+
static String declaration(final String name, final Variable v,
125+
final List<String> types)
126+
{
127+
final Object value = v.value().orElse(null);
128+
// A Java class (e.g. after "from java.util import ArrayList"): itself
129+
final Class<?> c = value instanceof PyJavaType ? ((PyJavaType) value)
130+
.getProxyType() : value instanceof Class ? (Class<?>) value : null;
131+
if (c != null) {
132+
if (!isNamed(c)) return name + " = " + any(types);
133+
types.add(c.getName());
134+
return name + " = " + c.getName().replace('$', '.');
135+
}
136+
// A Python module (e.g. after "import os"): imported again
137+
if (value instanceof PyModule) {
138+
final PyObject module = ((PyModule) value).__findattr__("__name__");
139+
if (module != null && module.toString().matches("[\\w.]+")) {
140+
return "import " + module + " as " + name;
141+
}
142+
}
143+
// A function (e.g. after "def f(): ...")
144+
if (value instanceof PyFunction) {
145+
return name + " = lambda *args, **kwargs: None";
146+
}
147+
// Another Python value (e.g. a dict): an instance of its builtin type
148+
if (value instanceof PyObject) {
149+
final String builtin = pythonType(((PyObject) value).getType()
150+
.getName());
151+
return name + " = " + (builtin != null ? builtin + "()" : any(types));
152+
}
153+
// A Java value, or a script parameter: an instance of its type
154+
final String t = parameterType(v.type());
155+
types.add(t);
156+
return name + " = " + t.replace('$', '.') + "()";
157+
}
158+
159+
/**
160+
* A value of any type (noting the module that says so), for variables
161+
* without a type to declare.
162+
*/
163+
private static String any(final List<String> types) {
164+
types.add("typing.Any");
165+
return "typing.cast(typing.Any, None)";
166+
}
167+
168+
/**
169+
* The Python 3 type of a Python 2 builtin one (as the Python server knows
170+
* Python 3's), e.g. {@code str} for {@code unicode}; or null if not a
171+
* builtin one.
172+
*/
173+
private static String pythonType(final String type) {
174+
switch (type) {
175+
case "int":
176+
case "long":
177+
return "int";
178+
case "unicode":
179+
return "str";
180+
case "bool":
181+
case "float":
182+
case "str":
183+
case "list":
184+
case "dict":
185+
case "tuple":
186+
case "set":
187+
return type;
188+
default:
189+
return null;
190+
}
191+
}
192+
193+
/** Whether code can refer to a class by its name. */
194+
private static boolean isNamed(final Class<?> c) {
195+
if (c.isPrimitive() || c.isArray() || c.isAnonymousClass() || c
196+
.isLocalClass() || c.getName().indexOf('.') < 0) return false;
197+
for (Class<?> t = c; t != null; t = t.getEnclosingClass()) {
198+
if (!Modifier.isPublic(t.getModifiers())) return false;
199+
}
200+
return true;
201+
}
202+
95203
/**
96204
* @param type the Java type (of a SciJava Script parameter) declared in the
97205
* script

‎src/test/java/org/scijava/plugins/scripting/jython/completion/JythonDialectTest.java‎

Lines changed: 63 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,18 @@
3939
import java.util.concurrent.TimeUnit;
4040
import java.util.stream.Collectors;
4141

42+
import javax.script.ScriptContext;
43+
import javax.script.ScriptEngine;
44+
4245
import org.eclipse.lsp4j.CompletionItem;
4346
import org.junit.After;
4447
import org.junit.Before;
4548
import org.junit.Test;
4649
import org.scijava.Context;
4750
import org.scijava.code.lsp.LanguageServerService;
51+
import org.python.jsr223.PyScriptEngineFactory;
4852
import org.scijava.code.lsp.ScriptDialect.Header;
53+
import org.scijava.code.lsp.ScriptDialect.Variable;
4954
import org.scijava.code.lsp.ScriptSession;
5055
import org.scijava.script.ScriptLanguage;
5156
import org.scijava.script.ScriptService;
@@ -101,11 +106,11 @@ public void testParameterType() {
101106
public void testHeader() {
102107
// Each parameter is declared as an instance of its type, whose package
103108
// is imported.
104-
final Map<String, Class<?>> parameters = new LinkedHashMap<>();
105-
parameters.put("d", java.math.BigDecimal.class);
106-
parameters.put("n", int.class);
107-
parameters.put("e", Map.Entry.class);
108-
parameters.put("a", double[].class);
109+
final Map<String, Variable> parameters = new LinkedHashMap<>();
110+
parameters.put("d", Variable.ofType(java.math.BigDecimal.class));
111+
parameters.put("n", Variable.ofType(int.class));
112+
parameters.put("e", Variable.ofType(Map.Entry.class));
113+
parameters.put("a", Variable.ofType(double[].class));
109114
final Header header = new JythonDialect().header(parameters);
110115
assertEquals("import array; import java.math; import java.util; " +
111116
"d = java.math.BigDecimal(); n = int(); " +
@@ -116,6 +121,31 @@ public void testHeader() {
116121
.hiddenNames().toArray()));
117122
}
118123

124+
@Test
125+
public void testLiveHeader() throws Exception {
126+
// A live interpreter's variables, by their values.
127+
final ScriptEngine engine = new PyScriptEngineFactory().getScriptEngine();
128+
engine.put("injected", new java.util.ArrayList<String>());
129+
engine.eval("import os\n" + //
130+
"from java.util import HashMap\n" + //
131+
"def helper(): pass\n" + //
132+
"pairs = {1: 2}\n");
133+
final Map<String, Variable> live = new LinkedHashMap<>();
134+
for (final String name : Arrays.asList("injected", "os", "HashMap",
135+
"helper", "pairs"))
136+
{
137+
final Object value = engine.get(name);
138+
live.put(name, Variable.ofValue(value, value.getClass()));
139+
}
140+
final Header header = new JythonDialect().header(live);
141+
assertEquals("import java.util; " + //
142+
"injected = java.util.ArrayList(); " + //
143+
"import os as os; " + //
144+
"HashMap = java.util.HashMap; " + //
145+
"helper = lambda *args, **kwargs: None; " + //
146+
"pairs = dict()", header.code());
147+
}
148+
119149
@Test
120150
public void testScriptParameters() {
121151
final String header = "" + //
@@ -149,11 +179,37 @@ public void testAssignmentShadowsScriptParameter() {
149179
}
150180

151181

182+
@Test
183+
public void testInterpreterBindings() throws Exception {
184+
// Code typed into a running interpreter: its variables.
185+
final ScriptEngine engine = new PyScriptEngineFactory().getScriptEngine();
186+
engine.put("injected", new java.util.ArrayList<String>());
187+
engine.eval("from java.util import Collections, HashMap\n" +
188+
"m = HashMap()\n" +
189+
"def helper(): pass\n" +
190+
"import os.path as osp\n");
191+
final ScriptContext live = engine.getContext();
192+
assertTrue(complete("m.putIfAb", live).contains("putIfAbsent"));
193+
assertTrue(complete("injected.si", live).contains("size"));
194+
assertTrue(complete("x = hel", live).contains("helper"));
195+
// An imported Java class offers its static members.
196+
assertTrue(complete("Collections.emptyL", live).contains("emptyList"));
197+
// A module bound under another name is the module itself.
198+
assertTrue(complete("osp.joi", live).contains("join"));
199+
}
200+
152201
// -- Helper methods --
153202

154203
private List<String> complete(final String code) {
155-
try (ScriptSession session = new ScriptSession(context.service(
156-
LanguageServerService.class), jython))
204+
return complete(code, null);
205+
}
206+
207+
/** Completions at the end of the code, typed into the given interpreter. */
208+
private List<String> complete(final String code, final ScriptContext live) {
209+
final LanguageServerService servers = context.service(
210+
LanguageServerService.class);
211+
try (ScriptSession session = live == null ? new ScriptSession(servers,
212+
jython) : new ScriptSession(servers, jython, live))
157213
{
158214
return session.completion(code, code.length()).get(30, TimeUnit.SECONDS)
159215
.getItems().stream().map(CompletionItem::getLabel).collect(Collectors

0 commit comments

Comments
 (0)