Skip to content

Commit 657bb21

Browse files
gselzerclaude
andcommitted
Serve plain Python: sessions declare the parameters
Jython's analysis and the Python server beside it now read the same text, which the session makes in JythonDialect: parameters are declared as instances of their classes (e.g. img = net.imglib2.img.Img()), which both understand. So the server no longer reads #@ lines, and PythonCompanion no longer transforms scripts for jedi. Jython's analysis now resolves classes by their full names, and the stubs follow imports separated by semicolons. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent be381ca commit 657bb21

7 files changed

Lines changed: 89 additions & 97 deletions

File tree

‎src/main/java/org/scijava/plugins/scripting/jython/JavaStubs.java‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,8 @@ public CompletableFuture<Void> ensure(final Collection<String> packages) {
182182
*/
183183
public static Set<String> imports(final String script) {
184184
final Set<String> packages = new TreeSet<>();
185-
for (final String line : script.split("\n")) {
185+
// NB: Statements on one line are separated by semicolons.
186+
for (final String line : script.split("[\n;]")) {
186187
final String l = line.trim();
187188
String module = null;
188189
if (l.startsWith("from ")) {

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

Lines changed: 9 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -38,14 +38,12 @@
3838

3939
/**
4040
* How Jython scripts see their script parameters: as the Java objects
41-
* themselves (primitives boxed), e.g. {@code image: net.imglib2.img.Img}; or
42-
* as Python values, where Jython converts them (e.g. strings).
41+
* themselves (primitives boxed), e.g. {@code net.imglib2.img.Img}.
4342
* <p>
44-
* Run-time types are Java class names (e.g. {@code java.lang.String}), as
45-
* Jython's own completion needs them; declarations are Python, for a Python
46-
* language server knowing the Java packages from stubs (see
47-
* {@link JavaStubs}), e.g. {@code import net.imglib2.img; image:
48-
* net.imglib2.img.Img; name: str}.
43+
* Each is declared as an instance of its class, in code both Jython's own
44+
* analysis (Python 2) and a Python language server knowing the Java packages
45+
* from stubs (see {@link JavaStubs}) read: e.g. {@code import net.imglib2.img;
46+
* image = net.imglib2.img.Img()}. (Arrays are lists.)
4947
* </p>
5048
*
5149
* @author Gabriel Selzer
@@ -59,7 +57,8 @@ public String runtimeType(final Class<?> type) {
5957

6058
@Override
6159
public String declare(final String name, final String type) {
62-
return name + ": " + pythonType(type);
60+
return name + " = " + (type.startsWith("[") ? "list" : type.replace('$',
61+
'.')) + "()";
6362
}
6463

6564
@Override
@@ -73,38 +72,12 @@ public List<String> imports(final Collection<String> types) {
7372
static Set<String> packages(final Collection<String> types) {
7473
final Set<String> packages = new TreeSet<>();
7574
for (final String t : types) {
76-
if (pythonType(t).equals(t.replace('$', '.')) && t.lastIndexOf(
77-
'.') > 0) packages.add(t.substring(0, t.lastIndexOf('.')));
75+
if (!t.startsWith("[") && t.lastIndexOf('.') > 0) packages.add(t
76+
.substring(0, t.lastIndexOf('.')));
7877
}
7978
return packages;
8079
}
8180

82-
/**
83-
* The Python type of a Java class name, as Jython converts its values:
84-
* e.g. {@code str} for {@code java.lang.String}.
85-
*/
86-
static String pythonType(final String type) {
87-
switch (type) {
88-
case "java.lang.String":
89-
case "java.lang.Character":
90-
return "str";
91-
case "java.lang.Integer":
92-
case "java.lang.Long":
93-
case "java.lang.Short":
94-
case "java.lang.Byte":
95-
case "java.math.BigInteger":
96-
return "int";
97-
case "java.lang.Double":
98-
case "java.lang.Float":
99-
case "java.math.BigDecimal":
100-
return "float";
101-
case "java.lang.Boolean":
102-
return "bool";
103-
default:
104-
return type.startsWith("[") ? "list" : type.replace('$', '.');
105-
}
106-
}
107-
10881
/** Maps primitive types to their wrappers, whose members can be listed. */
10982
private static Class<?> boxed(final Class<?> type) {
11083
if (!type.isPrimitive()) return type;

‎src/main/java/org/scijava/plugins/scripting/jython/JythonLanguageServer.java‎

Lines changed: 13 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,6 @@
5858
import org.python.core.PyObject;
5959
import org.python.core.PyString;
6060
import org.scijava.code.lsp.jvm.ClassIndex;
61-
import org.scijava.code.lsp.LanguageServerService;
6261
import org.scijava.code.lsp.jvm.Callable;
6362
import org.scijava.code.lsp.Environment;
6463
import org.scijava.code.lsp.jvm.ScriptLanguageServer;
@@ -83,40 +82,37 @@
8382
*/
8483
public class JythonLanguageServer extends ScriptLanguageServer {
8584

86-
/** How scripts see their parameters: as the Java objects themselves. */
85+
/**
86+
* How scripts see their parameters: as the Java objects themselves,
87+
* declared in the text (see {@link JythonServerPlugin#dialect()}).
88+
*/
8789
static final JythonDialect DIALECT = new JythonDialect();
8890

8991
static {
9092
// Warm the class index in the background, as completion needs it.
9193
new Thread(ClassIndex::ensureCache, "Jython-ClassIndex-warmup").start();
9294
}
9395

94-
private final LanguageServerService servers;
9596
private final JythonAutoCompletions engine = new JythonAutoCompletions();
9697

9798
/** A Python language server beside Jython's analysis, or null if none. */
9899
private final PythonCompanion python;
99100

100101
/**
101-
* @param servers Reads the scripts' parameters.
102102
* @param environment Where the scripts run (e.g. a live interpreter).
103103
*/
104-
public JythonLanguageServer(final LanguageServerService servers,
105-
final Environment environment)
106-
{
107-
this(servers, environment, null);
104+
public JythonLanguageServer(final Environment environment) {
105+
this(environment, null);
108106
}
109107

110108
/**
111-
* @param servers Reads the scripts' parameters.
112109
* @param environment Where the scripts run (e.g. a live interpreter).
113110
* @param python A Python language server beside Jython's analysis, or null.
114111
*/
115-
JythonLanguageServer(final LanguageServerService servers,
116-
final Environment environment, final PythonCompanion python)
112+
JythonLanguageServer(final Environment environment,
113+
final PythonCompanion python)
117114
{
118115
super(environment);
119-
this.servers = servers;
120116
this.python = python;
121117
}
122118

@@ -310,24 +306,16 @@ private JythonAutoCompletions.Result completionsAt(final String text,
310306
'\n') + 1);
311307
return engine.completionsFor(textToCaret.substring(0, textToCaret
312308
.length() - lastLine.length()), lastLine, alreadyEnteredText(lastLine),
313-
predefinedVariables(text));
309+
liveVariables());
314310
}
315311

316312
/**
317-
* Gets the variables defined before the script's code runs: script
318-
* parameters declared via {@code #@} lines, and, when completing in a live
319-
* interpreter, its bindings (which reflect actual runtime values, so they
320-
* take precedence).
313+
* Gets the variables defined before the code runs, when completing in a
314+
* live interpreter: its bindings. (A script's parameters are declared in
315+
* its text.)
321316
*/
322-
private Map<String, DotAutocompletions> predefinedVariables(
323-
final String text)
324-
{
317+
private Map<String, DotAutocompletions> liveVariables() {
325318
final Map<String, DotAutocompletions> vars = new LinkedHashMap<>();
326-
if (servers != null) {
327-
servers.scriptParameters(text).forEach((name,
328-
type) -> vars.put(name, new VarDotAutocompletions(DIALECT.runtimeType(
329-
type))));
330-
}
331319
final ScriptContext live = environment().live();
332320
final Bindings bindings = live == null ? null : live.getBindings(
333321
ScriptContext.ENGINE_SCOPE);

‎src/main/java/org/scijava/plugins/scripting/jython/JythonScriptParser.java‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,13 @@ static public DotAutocompletions parseRight(final PyObject right, final Scope sc
404404
// gray8 = IJ.getImage().GRAY8
405405
final Attribute attr = (Attribute)right;
406406
final DotAutocompletions da = parseRight(attr.getValue(), scope);
407+
// e.g. a class by its full name, after a package (or nothing known)
408+
// img = net.imglib2.img.Img()
409+
if (DotAutocompletions.EMPTY == da || da instanceof StaticDotAutocompletions) {
410+
final String fullName = javaClass(attr);
411+
if (null != fullName)
412+
return new StaticDotAutocompletions(fullName);
413+
}
407414
if (DotAutocompletions.EMPTY == da)
408415
return da;
409416
final String name = attr.getInternalAttr();
@@ -465,4 +472,32 @@ static public DotAutocompletions parseRight(final PyObject right, final Scope sc
465472

466473
return DotAutocompletions.EMPTY;
467474
}
475+
476+
/**
477+
* The Java class a dotted name (e.g. {@code java.util.Map.Entry}) is, by
478+
* its full name; or null if none (e.g. it starts with a variable).
479+
*/
480+
static private String javaClass(final Attribute attr) {
481+
final StringBuilder name = new StringBuilder(attr.getInternalAttr());
482+
PyObject node = attr.getValue();
483+
while (node instanceof Attribute) {
484+
name.insert(0, ((Attribute)node).getInternalAttr() + ".");
485+
node = ((Attribute)node).getValue();
486+
}
487+
if (!(node instanceof Name)) return null;
488+
name.insert(0, ((Name)node).getInternalId() + ".");
489+
// NB: A package starts lowercase; a class, uppercase.
490+
if (!Character.isLowerCase(name.charAt(0))) return null;
491+
String candidate = name.toString();
492+
while (true) {
493+
try {
494+
return Class.forName(candidate).getName();
495+
} catch (ClassNotFoundException | LinkageError e) {
496+
// Perhaps a nested class: e.g. java.util.Map$Entry.
497+
final int dot = candidate.lastIndexOf('.');
498+
if (dot < 0 || !Character.isUpperCase(candidate.charAt(candidate.lastIndexOf('.', dot - 1) + 1))) return null;
499+
candidate = candidate.substring(0, dot) + "$" + candidate.substring(dot + 1);
500+
}
501+
}
502+
}
468503
}

‎src/main/java/org/scijava/plugins/scripting/jython/JythonServerPlugin.java‎

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,16 @@
3535
import org.scijava.code.lsp.LanguageServerService;
3636
import org.scijava.code.lsp.Environment;
3737
import org.scijava.code.lsp.LanguageServerPlugin;
38+
import org.scijava.code.lsp.script.ScriptDialect;
3839
import org.scijava.plugin.Parameter;
3940
import org.scijava.plugin.Plugin;
4041
import org.scijava.script.ScriptLanguage;
4142

4243
/**
4344
* Offers Jython's in-process language server (see
4445
* {@link JythonLanguageServer}), with a Python language server beside it if
45-
* one is offered (see {@link PythonCompanion}).
46+
* one is offered (see {@link PythonCompanion}); both see scripts as plain
47+
* Python (see {@link JythonDialect}).
4648
*
4749
* @author Gabriel Selzer
4850
*/
@@ -62,8 +64,13 @@ public boolean supports(final ScriptLanguage language) {
6264

6365
@Override
6466
public LanguageServer create(final Environment environment) {
65-
return new JythonLanguageServer(servers, environment, PythonCompanion
66-
.launch(servers, stubs()));
67+
return new JythonLanguageServer(environment, PythonCompanion.launch(
68+
servers, stubs()));
69+
}
70+
71+
@Override
72+
public ScriptDialect dialect() {
73+
return JythonLanguageServer.DIALECT;
6774
}
6875

6976
/** Sets the stubs, e.g. to write them elsewhere in tests. */

‎src/main/java/org/scijava/plugins/scripting/jython/PythonCompanion.java‎

Lines changed: 10 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,9 @@
3333
import java.util.Comparator;
3434
import java.util.HashSet;
3535
import java.util.List;
36-
import java.util.Map;
3736
import java.util.Set;
38-
import java.util.TreeSet;
3937
import java.util.concurrent.CompletableFuture;
4038
import java.util.function.Function;
41-
import java.util.stream.Collectors;
4239

4340
import org.eclipse.lsp4j.CompletionItem;
4441
import org.eclipse.lsp4j.CompletionList;
@@ -57,15 +54,14 @@
5754
import org.eclipse.lsp4j.services.TextDocumentService;
5855
import org.scijava.code.lsp.Environment;
5956
import org.scijava.code.lsp.LanguageServerService;
60-
import org.scijava.code.lsp.script.ScriptDocument;
61-
import org.scijava.code.lsp.script.TransformingLanguageServer;
6257

6358
/**
6459
* A Python language server (e.g. jedi-language-server, if the
6560
* {@code appose-python} scripting plugin offers one) beside Jython's own
6661
* analysis (see {@link JythonLanguageServer}): for Python modules, and for
67-
* Java classes, from Python stubs of the Java packages a script imports and
68-
* its parameters' types (see {@link JavaStubs}, written in the background).
62+
* Java classes, from Python stubs of the Java packages a script imports
63+
* (including those of its parameters' types, declared in its text; see
64+
* {@link JavaStubs}, written in the background).
6965
* <p>
7066
* The server analyzes scripts as CPython (Jython's standard library is Python
7167
* 2's, but mostly the same): so its completions follow Jython's own, it is
@@ -77,36 +73,30 @@
7773
*/
7874
final class PythonCompanion {
7975

80-
/** The server, seeing scripts as plain Python. */
76+
/** The server. */
8177
private final LanguageServer server;
8278
private final JavaStubs stubs;
83-
private final Function<String, Map<String, Class<?>>> parameters;
8479

85-
private PythonCompanion(final LanguageServer python, final JavaStubs stubs,
86-
final Function<String, Map<String, Class<?>>> parameters)
87-
{
80+
private PythonCompanion(final LanguageServer python, final JavaStubs stubs) {
8881
this.stubs = stubs;
89-
this.parameters = parameters;
9082
// NB: No client: the server's problems are not asked for.
91-
server = new TransformingLanguageServer(python, text -> ScriptDocument.of(
92-
text, parameters.apply(text), JythonLanguageServer.DIALECT, null));
83+
server = python;
9384
server.initialize(new InitializeParams()).thenRun(() -> server
9485
.initialized(new InitializedParams()));
9586
}
9687

9788
/**
9889
* Gets a Python server beside Jython's, or null if none is offered.
9990
*
100-
* @param servers Offers the server, and reads the scripts' parameters.
91+
* @param servers Offers the server.
10192
* @param stubs The stubs of Java packages, for the server to search.
10293
*/
10394
static PythonCompanion launch(final LanguageServerService servers,
10495
final JavaStubs stubs)
10596
{
10697
if (servers == null) return null;
10798
final LanguageServer python = servers.launch("python", environment(stubs));
108-
return python == null ? null : new PythonCompanion(python, stubs,
109-
servers::scriptParameters);
99+
return python == null ? null : new PythonCompanion(python, stubs);
110100
}
111101

112102
/** Where the server runs: its own Python, searching the Java stubs. */
@@ -192,14 +182,9 @@ private TextDocumentService documents() {
192182
return server.getTextDocumentService();
193183
}
194184

195-
/** Writes the stubs the script needs, in the background. */
185+
/** Writes the stubs of the packages the script imports, in the background. */
196186
private void ensureStubs(final String text) {
197-
final Set<String> packages = new TreeSet<>(JavaStubs.imports(text));
198-
final ScriptDocument doc = ScriptDocument.of(text, parameters.apply(text),
199-
JythonLanguageServer.DIALECT, null);
200-
packages.addAll(JythonDialect.packages(doc.parameters().values().stream()
201-
.filter(t -> t != null).collect(Collectors.toList())));
202-
stubs.ensure(packages);
187+
stubs.ensure(JavaStubs.imports(text));
203188
}
204189

205190
/**

‎src/test/java/org/scijava/plugins/scripting/jython/JythonServerCompletionTest.java‎

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -118,15 +118,18 @@ public void testMerge() {
118118
@Test
119119
public void testDeclarations() {
120120
final JythonDialect dialect = new JythonDialect();
121-
assertEquals("img: net.imglib2.img.Img", dialect.declare("img",
121+
assertEquals("img = net.imglib2.img.Img()", dialect.declare("img",
122122
"net.imglib2.img.Img"));
123-
assertEquals("n: int", dialect.declare("n", dialect.runtimeType(
124-
int.class)));
125-
assertEquals("e: java.util.Map.Entry", dialect.declare("e",
123+
assertEquals("n = java.lang.Integer()", dialect.declare("n", dialect
124+
.runtimeType(int.class)));
125+
assertEquals("e = java.util.Map.Entry()", dialect.declare("e",
126126
"java.util.Map$Entry"));
127-
assertEquals(Arrays.asList("import java.util", "import net.imglib2.img"),
128-
dialect.imports(Arrays.asList("net.imglib2.img.Img", "java.lang.String",
129-
"java.util.Map$Entry")));
127+
assertEquals("a = list()", dialect.declare("a", dialect.runtimeType(
128+
double[].class)));
129+
assertEquals(Arrays.asList("import java.lang", "import java.util",
130+
"import net.imglib2.img"), dialect.imports(Arrays.asList(
131+
"net.imglib2.img.Img", "java.lang.String", "java.util.Map$Entry",
132+
"[D")));
130133
assertTrue(JythonServerPlugin.isJython(jython));
131134
}
132135
}

0 commit comments

Comments
 (0)