Skip to content

Commit c1f7834

Browse files
authored
[pythonscripting] Add scope typings and update helper lib to version 1.0.20 (#21214)
Signed-off-by: Holger Hees <holger.hees@gmail.com>
1 parent 1a2a3d1 commit c1f7834

8 files changed

Lines changed: 184 additions & 38 deletions

File tree

bundles/org.openhab.automation.pythonscripting/README.md

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,18 @@ As a final step, the folders `/openhab/conf/automation/python/libs/` and `/openh
220220

221221
![Pythonscripting autocompletion](doc/ide_autocompletion.png)
222222

223+
#### Autocompletion on dynamic results with custom type hints
224+
225+
Typings are not working on methods with an dynamic result types like `Registry.getItemState`. To force a specific expected result type, you can annotate the variable like below.
226+
227+
```python
228+
from org.openhab.core.library.types import DecimalType
229+
230+
item: DecimalType = Registry.getItemState("TestNumberItem")
231+
```
232+
233+
![Pythonscripting autocompletion](doc/ide_autocompletion_custom.png)
234+
223235
## Typical log errors
224236

225237
### Graal python language not initialized. ...
@@ -279,4 +291,10 @@ You should also check your logs for a message related to the helper lib deployme
279291
280292
## Limitations
281293
282-
- GraalPy can't handle arguments in constructors of Java objects. Means you can't instantiate a Java object in Python with a parameter. <https://github.com/oracle/graalpython/issues/367>
294+
### Avoid using datetime.strptime
295+
296+
You should avoid using `datetime.strptime` because there is a bug in GraalPy associated with it. Once this function is used, it can change the JVM-wide default time zone for the whole openHAB process until it is reset or openHAB is restarted.
297+
298+
### Constructors of Java objects
299+
300+
GraalPy can't handle arguments in constructors of Java objects. Means you can't instantiate a Java object in Python with a parameter. <https://github.com/oracle/graalpython/issues/367>
44.2 KB
Loading

bundles/org.openhab.automation.pythonscripting/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
<name>openHAB Add-ons :: Bundles :: Automation :: Python Scripting</name>
1616

1717
<properties>
18-
<helperlib.version>1.0.19</helperlib.version>
18+
<helperlib.version>1.0.20</helperlib.version>
1919
</properties>
2020

2121
<dependencies>

bundles/org.openhab.automation.pythonscripting/src/main/java/org/openhab/automation/pythonscripting/internal/PythonScriptEngine.java

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import java.util.Arrays;
3030
import java.util.HashSet;
3131
import java.util.List;
32+
import java.util.Map;
3233
import java.util.Set;
3334
import java.util.concurrent.locks.Lock;
3435
import java.util.concurrent.locks.ReentrantLock;
@@ -291,7 +292,7 @@ public void accept(Path path) {
291292
getBindings(ScriptContext.ENGINE_SCOPE).put(ScriptExtensionModuleProvider.IMPORT_PROXY_NAME,
292293
wrapImportFn);
293294
try {
294-
if (!isScriptFile() && !isScriptModule() && !isTransformation()) {
295+
if (!isScriptFile() && !isScriptModule() && !isTransformation() && !isCLI()) {
295296
logger.warn(
296297
"Unknown script environment detected for engine '{}': Neither script file, script module nor transformation.",
297298
engineIdentifier);
@@ -540,6 +541,20 @@ private boolean isTransformation() {
540541
return engineIdentifier.startsWith(OPENHAB_TRANSFORMATION_SCRIPT);
541542
}
542543

544+
/**
545+
* Tests if the script is used in the karaf console, i.e. created from the ConsoleCommandExtension service.
546+
*
547+
* @return true if it is a cli script, false otherwise
548+
*/
549+
private boolean isCLI() {
550+
ScriptContext ctx = getContext();
551+
if (ctx == null) {
552+
logger.warn("Failed to retrieve script context from engine '{}'.", engineIdentifier);
553+
return false;
554+
}
555+
return this.engineIdentifier.startsWith("pythonscripting-cli");
556+
}
557+
543558
private static Set<String> transformArrayToSet(Value value) {
544559
try {
545560
Set<String> set = new HashSet<>();
@@ -590,4 +605,8 @@ private static ZonedDateTime parseDatetime(Value value) {
590605
? OffsetDateTime.now().getOffset().getId()
591606
: ""));
592607
}
608+
609+
public Map<String, Object> getScope() {
610+
return scriptExtensionModuleProvider.getScope();
611+
}
593612
}

bundles/org.openhab.automation.pythonscripting/src/main/java/org/openhab/automation/pythonscripting/internal/console/PythonConsoleCommandExtension.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ private void executeTyping(Console console) {
229229
+ PythonScriptEngineConfiguration.PYTHON_TYPINGS_PATH + "'.")) {
230230
return;
231231
}
232-
new TypingCmd(new TypingCmd.Logger(console)).build();
232+
new TypingCmd(new TypingCmd.Logger(console), scriptEngineManager).build();
233233
} catch (Exception e) {
234234
throw new IllegalArgumentException(e);
235235
}
@@ -286,7 +286,7 @@ private void printLoadingMessage(Console console, boolean show) {
286286
* including any injected required modules.
287287
*/
288288
private @Nullable Object executePython(Console console, EngineEvalFunction process, boolean withFullContext) {
289-
String scriptIdentifier = "python-console-" + UUID.randomUUID().toString();
289+
String scriptIdentifier = "pythonscripting-cli-" + UUID.randomUUID().toString();
290290
ScriptEngine engine = null;
291291

292292
try {

bundles/org.openhab.automation.pythonscripting/src/main/java/org/openhab/automation/pythonscripting/internal/console/handler/TypingCmd.java

Lines changed: 109 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,19 +19,28 @@
1919
import java.nio.file.Path;
2020
import java.nio.file.Paths;
2121
import java.util.ArrayList;
22+
import java.util.Collection;
2223
import java.util.Comparator;
2324
import java.util.HashMap;
2425
import java.util.HashSet;
2526
import java.util.Map;
27+
import java.util.Map.Entry;
2628
import java.util.Set;
29+
import java.util.UUID;
2730
import java.util.stream.Collectors;
2831
import java.util.stream.Stream;
2932

33+
import javax.script.ScriptEngine;
34+
3035
import org.eclipse.jdt.annotation.NonNullByDefault;
36+
import org.openhab.automation.pythonscripting.internal.PythonScriptEngine;
3137
import org.openhab.automation.pythonscripting.internal.PythonScriptEngineConfiguration;
38+
import org.openhab.automation.pythonscripting.internal.PythonScriptEngineFactory;
3239
import org.openhab.automation.pythonscripting.internal.console.handler.typing.ClassCollector;
3340
import org.openhab.automation.pythonscripting.internal.console.handler.typing.ClassCollector.ClassContainer;
3441
import org.openhab.automation.pythonscripting.internal.console.handler.typing.ClassConverter;
42+
import org.openhab.core.automation.module.script.ScriptEngineContainer;
43+
import org.openhab.core.automation.module.script.ScriptEngineManager;
3544
import org.openhab.core.io.console.Console;
3645

3746
/**
@@ -42,11 +51,14 @@
4251
@NonNullByDefault
4352
public class TypingCmd {
4453
private final Logger logger;
54+
private final ScriptEngineManager scriptEngineManager;
4555

56+
private static final String OPENHAB_PACKAGE_PREFIX = "org.openhab";
4657
private static final String PATH_SEPARATOR = FileSystems.getDefault().getSeparator();
4758

48-
public TypingCmd(Logger logger) {
59+
public TypingCmd(Logger logger, ScriptEngineManager scriptEngineManager) {
4960
this.logger = logger;
61+
this.scriptEngineManager = scriptEngineManager;
5062
}
5163

5264
public void build() throws Exception {
@@ -62,17 +74,28 @@ public void build() throws Exception {
6274

6375
Map<String, ClassContainer> fileContainerMap = new HashMap<String, ClassContainer>();
6476
Set<String> imports = new HashSet<String>();
65-
// Collect Bundle Classes
66-
Map<String, ClassContainer> bundleClassMap = collector.collectBundleClasses("org.openhab");
77+
Set<String> dumped = new HashSet<String>();
78+
79+
// 1. The bundle collector is collecting all public openhab classes and all used/imported classes
80+
Map<String, ClassContainer> bundleClassMap = collector.collectBundleClasses(OPENHAB_PACKAGE_PREFIX);
81+
82+
// 2. The scope object is dumped
83+
// All referenced openhab classes which are not already collected, are "registered" in the bundleClassMap
84+
// And all used/imported class are added to the import list
85+
Collection<String> scopeImports = dumpScope(outputPath, bundleClassMap);
86+
imports.addAll(scopeImports);
87+
88+
// 3. All openhab classes are dumped
6789
for (ClassContainer container : bundleClassMap.values()) {
6890
ClassConverter converter = new ClassConverter(container);
6991
String classBody = converter.build();
7092
imports.addAll(converter.getImports());
7193
dumpClassContentToFile(classBody, container, outputPath, fileContainerMap);
94+
dumped.add(container.getRelatedClass().getName());
7295
}
7396

74-
imports = imports.stream().filter(i -> !i.startsWith("org.openhab")).collect(Collectors.toSet());
75-
97+
// 4. All collected imports are dumped, if they are not already dumped before
98+
imports = imports.stream().filter(i -> !dumped.contains(i)).collect(Collectors.toSet());
7699
Map<String, ClassContainer> reflectionClassMap = collector.collectReflectionClasses(imports);
77100
for (ClassContainer container : reflectionClassMap.values()) {
78101
ClassConverter converter = new ClassConverter(container);
@@ -88,6 +111,86 @@ public void build() throws Exception {
88111
+ outputPath + "'");
89112
}
90113

114+
private Collection<String> dumpScope(Path outputPath, Map<String, ClassContainer> bundleClassMap)
115+
throws IOException {
116+
Map<String, String> imports = new HashMap<String, String>();
117+
String identifier = "pythonscripting-cli-" + UUID.randomUUID().toString();
118+
try {
119+
ScriptEngineContainer container = scriptEngineManager
120+
.createScriptEngine(PythonScriptEngineFactory.SCRIPT_TYPE, identifier);
121+
if (container != null) {
122+
StringBuilder scopeBody = new StringBuilder();
123+
ScriptEngine engine = container.getScriptEngine();
124+
125+
Map<String, Object> scope = ((PythonScriptEngine) engine).getScope();
126+
for (Entry<String, Object> entry : scope.entrySet()) {
127+
Object value = entry.getValue();
128+
Class cls;
129+
String packageName;
130+
String pythonClassName;
131+
String pythonModuleName;
132+
String definition;
133+
134+
if (value instanceof Class) {
135+
cls = (Class) value;
136+
packageName = cls.getName();
137+
138+
pythonClassName = ClassContainer.parsePythonClassName(packageName);
139+
pythonModuleName = ClassContainer.parsePythonModuleName(packageName);
140+
definition = entry.getKey() + ": Type[_" + pythonClassName + "] = _" + pythonClassName;
141+
} else {
142+
cls = value.getClass();
143+
packageName = value.getClass().getName();
144+
145+
if ("org.openhab.automation.pythonscripting.internal.provider.LifecycleTracker"
146+
.equals(packageName)) {
147+
packageName = "org.openhab.core.automation.module.script.LifecycleScriptExtensionProvider$LifecycleTracker";
148+
} else if (packageName.endsWith("Impl") || packageName.endsWith("Delegate")) {
149+
cls = value.getClass().getInterfaces()[0];
150+
packageName = cls.getName();
151+
}
152+
153+
pythonClassName = ClassContainer.parsePythonClassName(packageName);
154+
pythonModuleName = ClassContainer.parsePythonModuleName(packageName);
155+
156+
if (cls.isEnum()) {
157+
definition = entry.getKey() + ": _" + pythonClassName + " = " + "_" + pythonClassName + "."
158+
+ entry.getKey();
159+
} else {
160+
definition = entry.getKey() + ": _" + pythonClassName;
161+
}
162+
}
163+
164+
if (packageName.startsWith(OPENHAB_PACKAGE_PREFIX) && !bundleClassMap.containsKey(packageName)) {
165+
bundleClassMap.put(packageName, new ClassContainer(cls));
166+
}
167+
168+
imports.put(packageName,
169+
"from " + pythonModuleName + " import " + pythonClassName + " as _" + pythonClassName);
170+
171+
String classUrl = ClassConverter.buildDocumentationLink(packageName);
172+
173+
scopeBody.append(definition);
174+
scopeBody.append("\n");
175+
scopeBody.append("\"\"\"\n");
176+
scopeBody.append("Java class: ").append(packageName).append("\n\n");
177+
scopeBody.append("Java doc: ").append(classUrl).append("\n");
178+
scopeBody.append("\"\"\"\n\n");
179+
}
180+
181+
scopeBody.insert(0, "\n\n");
182+
scopeBody.insert(0, "from typing import Type");
183+
scopeBody.insert(0, ClassConverter.buildClassImports(imports.values()));
184+
185+
dumpContentToFile(scopeBody.toString(), outputPath.resolve("scope.py"));
186+
}
187+
} finally {
188+
scriptEngineManager.removeEngine(identifier);
189+
}
190+
191+
return imports.keySet();
192+
}
193+
91194
public void dumpInit(String path, Map<String, ClassContainer> fileContainerMap) throws IOException {
92195
File root = new File(path);
93196
File[] list = root.listFiles();
@@ -105,7 +208,7 @@ public void dumpInit(String path, Map<String, ClassContainer> fileContainerMap)
105208
StringBuilder initBody = new StringBuilder();
106209
// List<String> modules = new ArrayList<String>();
107210
for (File file : files) {
108-
if (file.toString().endsWith("__init__.py")) {
211+
if (file.toString().endsWith("__init__.py") || file.toString().endsWith("scope.py")) {
109212
continue;
110213
}
111214
ClassContainer container = fileContainerMap.get(file.toString());

bundles/org.openhab.automation.pythonscripting/src/main/java/org/openhab/automation/pythonscripting/internal/console/handler/typing/ClassConverter.java

Lines changed: 29 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ public int compare(MethodContainer o1, MethodContainer o2) {
112112
// Class imports
113113
if (!imports.isEmpty()) {
114114
classBody.insert(0, "\n\n");
115-
classBody.insert(0, buildClassImports());
115+
classBody.insert(0, buildClassImports(imports.values()));
116116
}
117117

118118
return classBody.toString();
@@ -263,28 +263,6 @@ private String buildClassMethod(MethodContainer method) {
263263
return builder.toString();
264264
}
265265

266-
private Object buildClassImports() {
267-
StringBuilder builder = new StringBuilder();
268-
HashSet<String> hashSet = new HashSet<>(imports.values());
269-
ArrayList<String> sortedImports = new ArrayList<>(hashSet);
270-
Collections.sort(sortedImports, new Comparator<String>() {
271-
@Override
272-
public int compare(String o1, String o2) {
273-
if (o1.length() > o2.length()) {
274-
return 1;
275-
}
276-
if (o1.length() < o2.length()) {
277-
return -1;
278-
}
279-
return o1.compareTo(o2);
280-
}
281-
});
282-
for (String importLine : sortedImports) {
283-
builder.append(importLine + "\n");
284-
}
285-
return builder.toString();
286-
}
287-
288266
private JavaType collectJavaTypes(Type genericType, Map<String, String> generics) {
289267
JavaType javaType = null;
290268
if (genericType instanceof TypeVariable) {
@@ -509,8 +487,7 @@ private String cleanClassName(String className) {
509487
return null;
510488
}
511489

512-
String classUrl = baseUrl
513-
+ container.getRelatedClass().getName().toLowerCase().replace(".", "/").replace("$", ".");
490+
String classUrl = buildDocumentationLink(container.getRelatedClass().getName());
514491

515492
StringBuilder builder = new StringBuilder();
516493
builder.append(" \"\"\"\n");
@@ -526,8 +503,7 @@ private String cleanClassName(String className) {
526503
return null;
527504
}
528505

529-
String classUrl = baseUrl
530-
+ container.getRelatedClass().getName().toLowerCase().replace(".", "/").replace("$", ".");
506+
String classUrl = buildDocumentationLink(container.getRelatedClass().getName());
531507

532508
StringBuilder builder = new StringBuilder();
533509
builder.append(" \"\"\"\n");
@@ -548,6 +524,32 @@ private String cleanClassName(String className) {
548524
return builder.toString();
549525
}
550526

527+
public static String buildDocumentationLink(String classname) {
528+
return baseUrl + classname.toLowerCase().replace(".", "/").replace("$", ".");
529+
}
530+
531+
public static String buildClassImports(Collection<String> imports) {
532+
StringBuilder builder = new StringBuilder();
533+
Set<String> hashSet = new HashSet<>(imports);
534+
List<String> sortedImports = new ArrayList<>(hashSet);
535+
Collections.sort(sortedImports, new Comparator<String>() {
536+
@Override
537+
public int compare(String o1, String o2) {
538+
if (o1.length() > o2.length()) {
539+
return 1;
540+
}
541+
if (o1.length() < o2.length()) {
542+
return -1;
543+
}
544+
return o1.compareTo(o2);
545+
}
546+
});
547+
for (String importLine : sortedImports) {
548+
builder.append(importLine + "\n");
549+
}
550+
return builder.toString();
551+
}
552+
551553
public static class JavaType {
552554
private final String type;
553555
private final List<JavaType> subTypes = new ArrayList<JavaType>();

bundles/org.openhab.automation.pythonscripting/src/main/java/org/openhab/automation/pythonscripting/internal/provider/ScriptExtensionModuleProvider.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,10 @@ public void put(String key, Object value) {
9696
this.globals.put(key, value);
9797
}
9898

99+
public Map<String, Object> getScope() {
100+
return new HashMap<>(this.globals);
101+
}
102+
99103
public static interface ModuleLocator {
100104
Map<String, Object> locateModule(String name, List<String> fromlist);
101105
}

0 commit comments

Comments
 (0)