From 611b644a10c040be83bfdcdbdfe51f6cf8ceff6f Mon Sep 17 00:00:00 2001 From: Paul Griffith Date: Wed, 9 Nov 2022 16:04:39 -0800 Subject: [PATCH 1/5] Expose DatasetBuilder and add kwargs for convenient instantiation --- .../extensions/common/DatasetExtensions.kt | 31 ++++ .../common/DatasetExtensions.properties | 5 + .../common/DatasetExtensionsTests.kt | 155 ++++++++++++++++++ .../org/imdc/extensions/common/JythonTest.kt | 18 +- 4 files changed, 207 insertions(+), 2 deletions(-) diff --git a/common/src/main/kotlin/org/imdc/extensions/common/DatasetExtensions.kt b/common/src/main/kotlin/org/imdc/extensions/common/DatasetExtensions.kt index b927a7d..f1bb64d 100644 --- a/common/src/main/kotlin/org/imdc/extensions/common/DatasetExtensions.kt +++ b/common/src/main/kotlin/org/imdc/extensions/common/DatasetExtensions.kt @@ -14,8 +14,15 @@ import org.apache.poi.ss.usermodel.CellType.STRING import org.apache.poi.ss.usermodel.DateUtil import org.apache.poi.ss.usermodel.WorkbookFactory import org.python.core.Py +import org.python.core.PyBoolean +import org.python.core.PyFloat import org.python.core.PyFunction +import org.python.core.PyInteger +import org.python.core.PyLong import org.python.core.PyObject +import org.python.core.PyString +import org.python.core.PyType +import org.python.core.PyUnicode import java.io.File import java.math.BigDecimal import java.util.Date @@ -405,4 +412,28 @@ object DatasetExtensions { left == right } } + + @ScriptFunction(docBundlePrefix = "DatasetExtensions") + @KeywordArgs( + names = ["**columns"], + types = [KeywordArgs::class], + ) + fun builder(args: Array, keywords: Array): DatasetBuilder { + if (args.size != keywords.size) throw Py.ValueError("builder must be called with only keyword arguments") + val colNames = keywords.toList() + val colTypes = args.mapIndexed { i, type -> + if (type !is PyType) { + throw Py.TypeError("${keywords[i]} was a ${type::class.simpleName}, but should be a type") + } + when (type) { + PyString.TYPE, PyUnicode.TYPE -> String::class.java + PyBoolean.TYPE -> Boolean::class.java + PyInteger.TYPE -> Int::class.java + PyLong.TYPE -> Long::class.java + PyFloat.TYPE -> Double::class.java + else -> type.toJava>() + } + } + return DatasetBuilder.newBuilder().colNames(colNames).colTypes(colTypes) + } } diff --git a/common/src/main/resources/org/imdc/extensions/common/DatasetExtensions.properties b/common/src/main/resources/org/imdc/extensions/common/DatasetExtensions.properties index b7048a9..9297d14 100644 --- a/common/src/main/resources/org/imdc/extensions/common/DatasetExtensions.properties +++ b/common/src/main/resources/org/imdc/extensions/common/DatasetExtensions.properties @@ -41,3 +41,8 @@ columnsEqual.param.dataset2=The second dataset. Must not be null. columnsEqual.param.ignoreCase=Pass True if the column names should be compared case-insensitive. Defaults to False. columnsEqual.param.includeTypes=Pass True if the column types must match as well. Defaults to True. columnsEqual.returns=True if the two datasets have the same columns, per additional parameters. + +builder.desc=Creates a new dataset using supplied column names and types. +builder.param.**columns=Optional. Keyword arguments can be supplied to predefine column names and types. The value of the argument should be a Java or Python type. +builder.returns=A DatasetBuilder object. Use addRow(value, ...) to add new values, and build() to construct the final dataset. \ + If keyword arguments were not supplied, column names and types can be manually declared using colNames() and colTypes(). diff --git a/common/src/test/kotlin/org/imdc/extensions/common/DatasetExtensionsTests.kt b/common/src/test/kotlin/org/imdc/extensions/common/DatasetExtensionsTests.kt index f52a0d0..4cf11aa 100644 --- a/common/src/test/kotlin/org/imdc/extensions/common/DatasetExtensionsTests.kt +++ b/common/src/test/kotlin/org/imdc/extensions/common/DatasetExtensionsTests.kt @@ -7,6 +7,8 @@ import io.kotest.assertions.withClue import io.kotest.matchers.shouldBe import org.imdc.extensions.common.DatasetExtensions.printDataset import org.python.core.Py +import java.awt.Color +import java.util.Date import kotlin.io.path.createTempFile import kotlin.io.path.writeBytes @@ -38,6 +40,9 @@ class DatasetExtensionsTests : JythonTest( } globals["xlsBytes"] = xlsSample globals["xlsFile"] = tempXls.toString() + + globals["date"] = Date::class.java + globals["color"] = Color::class.java }, ) { private fun Dataset.asClue(assertions: (Dataset) -> Unit) { @@ -289,5 +294,155 @@ class DatasetExtensionsTests : JythonTest( } } } + + context("Builder") { + test("Basic usage") { + eval("utils.builder(a=int, b=str, c=bool).addRow(1, '2', False).build()").asClue { + it.rowCount shouldBe 1 + it.columnCount shouldBe 3 + it.columnNames shouldBe listOf( + "a", + "b", + "c", + ) + it.columnTypes shouldBe listOf( + Int::class.java, + String::class.java, + Boolean::class.java, + ) + } + } + + test("Complex types") { + eval( + """ + utils.builder(date=date, color=color, unicode=unicode) \ + .addRow(date(), color.RED, u'test') \ + .build() + """.trimIndent(), + ).asClue { + it.rowCount shouldBe 1 + it.columnCount shouldBe 3 + it.columnNames shouldBe listOf( + "date", + "color", + "unicode", + ) + it.columnTypes shouldBe listOf( + Date::class.java, + Color::class.java, + String::class.java, + ) + } + } + + test("Nulls in rows") { + eval( + """ + utils.builder(a=int, b=str, c=bool) \ + .addRow(1, '2', False) \ + .addRow(None, None, None) \ + .build() + """.trimIndent(), + ).asClue { + it.rowCount shouldBe 2 + it.columnCount shouldBe 3 + it.columnNames shouldBe listOf( + "a", + "b", + "c", + ) + it.columnTypes shouldBe listOf( + Int::class.java, + String::class.java, + Boolean::class.java, + ) + } + } + + test("Empty dataset") { + eval("utils.builder().build()").asClue { + it.rowCount shouldBe 0 + it.columnCount shouldBe 0 + } + } + + test("Add a row as a list") { + eval( + """ + utils.builder(a=int, b=str, c=bool) \ + .addRow([1, '2', False]) \ + .build() + """.trimIndent(), + ).asClue { + it.rowCount shouldBe 1 + it.columnCount shouldBe 3 + } + } + + test("Add a row as a tuple") { + eval( + """ + utils.builder(a=int, b=str, c=bool) \ + .addRow((1, '2', False)) \ + .build() + """.trimIndent(), + ).asClue { + it.rowCount shouldBe 1 + it.columnCount shouldBe 3 + } + } + + test("Columns with complex names") { + eval( + """ + utils.builder(**{'a space': int, u'😍': bool, r',./;\'[]-=<>?:"{}|_+!@#%^&*()`~': str}) \ + .addRow((1, '2', False)) \ + .build() + """.trimIndent(), + ).asClue { + it.rowCount shouldBe 1 + it.columnCount shouldBe 3 + it.columnNames shouldBe listOf( + "a space", + "\uD83D\uDE0D", + """,./;\'[]-=<>?:"{}|_+!@#%^&*()`~""", + ) + } + } + + test("Invalid types") { + shouldThrowPyException(Py.TypeError) { + eval("utils.builder(a=1).build()") + } + shouldThrowPyException(Py.TypeError) { + eval("utils.builder(a=None).build()") + } + } + + test("Row without enough vararg values") { + shouldThrowPyException(Py.TypeError) { + eval( + """ + utils.builder(a=int, b=str, c=bool) \ + .addRow(1, '2') \ + .build() + """.trimIndent(), + ) + } + } + + test("Row without enough list values") { + shouldThrowPyException(Py.TypeError) { + eval( + """ + utils.builder(a=int, b=str, c=bool) \ + .addRow([1, '2']) \ + .build() + """.trimIndent(), + ) + } + } + } } } diff --git a/common/src/test/kotlin/org/imdc/extensions/common/JythonTest.kt b/common/src/test/kotlin/org/imdc/extensions/common/JythonTest.kt index 8faac6d..9144e89 100644 --- a/common/src/test/kotlin/org/imdc/extensions/common/JythonTest.kt +++ b/common/src/test/kotlin/org/imdc/extensions/common/JythonTest.kt @@ -1,10 +1,12 @@ package org.imdc.extensions.common import com.inductiveautomation.ignition.common.TypeUtilities +import io.kotest.assertions.fail import io.kotest.core.listeners.BeforeEachListener import io.kotest.core.spec.style.FunSpec import io.kotest.core.test.TestCase -import io.kotest.matchers.booleans.shouldBeTrue +import io.kotest.matchers.Matcher +import io.kotest.matchers.MatcherResult import org.intellij.lang.annotations.Language import org.python.core.CompileMode import org.python.core.CompilerFlags @@ -56,8 +58,10 @@ abstract class JythonTest(init: (globals: PyStringMap) -> Unit) : FunSpec() { try { case() } catch (exception: PyException) { - exception.match(type).shouldBeTrue() + PyExceptionTypeMatcher(exception).test(type) + return } + fail("Expected a $type to be thrown, but nothing was thrown") } companion object { @@ -75,5 +79,15 @@ abstract class JythonTest(init: (globals: PyStringMap) -> Unit) : FunSpec() { operator fun PyStringMap.set(key: String, value: Any?) { return __setitem__(key, Py.java2py(value)) } + + class PyExceptionTypeMatcher(val expected: PyException) : Matcher { + override fun test(value: PyObject): MatcherResult { + return MatcherResult( + expected.match(value), + failureMessageFn = { "Expected a $expected, but was $value" }, + negatedFailureMessageFn = { "Result should not be a $expected, but was $value" }, + ) + } + } } } From 612ae45ddd6a0e878c10477fd3a7e9e45e9efb62 Mon Sep 17 00:00:00 2001 From: Paul Griffith Date: Tue, 22 Nov 2022 14:52:23 -0800 Subject: [PATCH 2/5] Misc build improvements --- build.gradle.kts | 9 ++++----- client/build.gradle.kts | 10 +++------- common/build.gradle.kts | 10 +++------- designer/build.gradle.kts | 10 +++------- gateway/build.gradle.kts | 10 +++------- 5 files changed, 16 insertions(+), 33 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 3b45eae..7426825 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -2,7 +2,6 @@ plugins { alias(libs.plugins.kotlin) alias(libs.plugins.modl) - // alias(libs.plugins.dokka) TODO: Investigate Dokka for automatic generation of module docs } subprojects { @@ -21,10 +20,10 @@ ignitionModule { projectScopes.putAll( mapOf( - ":client" to "C", - ":common" to "GDC", - ":designer" to "D", - ":gateway" to "G", + projects.common.dependencyProject.path to "GDC", + projects.gateway.dependencyProject.path to "G", + projects.designer.dependencyProject.path to "D", + projects.client.dependencyProject.path to "C", ), ) diff --git a/client/build.gradle.kts b/client/build.gradle.kts index 7577b92..ddde6eb 100644 --- a/client/build.gradle.kts +++ b/client/build.gradle.kts @@ -3,14 +3,10 @@ plugins { kotlin("jvm") } -java { - toolchain { - languageVersion.set(JavaLanguageVersion.of(libs.versions.java.get())) - } -} - kotlin { - jvmToolchain(libs.versions.java.map(String::toInt).get()) + jvmToolchain { + languageVersion.set(libs.versions.java.map(JavaLanguageVersion::of)) + } } dependencies { diff --git a/common/build.gradle.kts b/common/build.gradle.kts index a925654..5fbc060 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -3,14 +3,10 @@ plugins { kotlin("jvm") } -java { - toolchain { - languageVersion.set(JavaLanguageVersion.of(libs.versions.java.get())) - } -} - kotlin { - jvmToolchain(libs.versions.java.map(String::toInt).get()) + jvmToolchain { + languageVersion.set(libs.versions.java.map(JavaLanguageVersion::of)) + } } dependencies { diff --git a/designer/build.gradle.kts b/designer/build.gradle.kts index fab5670..1149fe9 100644 --- a/designer/build.gradle.kts +++ b/designer/build.gradle.kts @@ -3,14 +3,10 @@ plugins { kotlin("jvm") } -java { - toolchain { - languageVersion.set(JavaLanguageVersion.of(libs.versions.java.get())) - } -} - kotlin { - jvmToolchain(libs.versions.java.map(String::toInt).get()) + jvmToolchain { + languageVersion.set(libs.versions.java.map(JavaLanguageVersion::of)) + } } dependencies { diff --git a/gateway/build.gradle.kts b/gateway/build.gradle.kts index 623b626..c6127a3 100644 --- a/gateway/build.gradle.kts +++ b/gateway/build.gradle.kts @@ -3,14 +3,10 @@ plugins { kotlin("jvm") } -java { - toolchain { - languageVersion.set(JavaLanguageVersion.of(libs.versions.java.get())) - } -} - kotlin { - jvmToolchain(libs.versions.java.map(String::toInt).get()) + jvmToolchain { + languageVersion.set(libs.versions.java.map(JavaLanguageVersion::of)) + } } dependencies { From 72a5120c5f41fae3803d3402a37f23067142ecce Mon Sep 17 00:00:00 2001 From: Paul Griffith Date: Tue, 22 Nov 2022 14:52:44 -0800 Subject: [PATCH 3/5] Improve null handling in PyObjectAppendable --- .../main/kotlin/org/imdc/extensions/common/Utilities.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/common/src/main/kotlin/org/imdc/extensions/common/Utilities.kt b/common/src/main/kotlin/org/imdc/extensions/common/Utilities.kt index 8939632..16981f0 100644 --- a/common/src/main/kotlin/org/imdc/extensions/common/Utilities.kt +++ b/common/src/main/kotlin/org/imdc/extensions/common/Utilities.kt @@ -9,15 +9,15 @@ import java.lang.reflect.Method class PyObjectAppendable(target: PyObject) : Appendable { private val writeMethod = target.__getattr__("write") - override fun append(csq: CharSequence): Appendable = this.apply { + override fun append(csq: CharSequence?): Appendable = apply { writeMethod.__call__(Py.newStringOrUnicode(csq.toString())) } - override fun append(csq: CharSequence, start: Int, end: Int): Appendable = this.apply { - append(csq.subSequence(start, end)) + override fun append(csq: CharSequence?, start: Int, end: Int): Appendable = apply { + append(csq.toString().subSequence(start, end)) } - override fun append(c: Char): Appendable = this.apply { + override fun append(c: Char): Appendable = apply { append(c.toString()) } } From abcd7f94887ddfb003ff4e4935c3c3a965dc7a2b Mon Sep 17 00:00:00 2001 From: Paul Griffith Date: Tue, 22 Nov 2022 15:04:36 -0800 Subject: [PATCH 4/5] Wrap DatasetBuilder to allow more efficient access to types --- .../org/imdc/extensions/client/ClientHook.kt | 7 +++ .../extensions/common/DatasetExtensions.kt | 9 ++- .../extensions/common/PyDatasetBuilder.kt | 57 +++++++++++++++++++ .../common/DatasetExtensionsTests.kt | 19 +++++++ .../imdc/extensions/designer/DesignerHook.kt | 7 +++ .../imdc/extensions/gateway/GatewayHook.kt | 7 ++- 6 files changed, 102 insertions(+), 4 deletions(-) create mode 100644 common/src/main/kotlin/org/imdc/extensions/common/PyDatasetBuilder.kt diff --git a/client/src/main/kotlin/org/imdc/extensions/client/ClientHook.kt b/client/src/main/kotlin/org/imdc/extensions/client/ClientHook.kt index 6b9d2dc..134c4b6 100644 --- a/client/src/main/kotlin/org/imdc/extensions/client/ClientHook.kt +++ b/client/src/main/kotlin/org/imdc/extensions/client/ClientHook.kt @@ -8,6 +8,7 @@ import com.inductiveautomation.ignition.common.script.ScriptManager import com.inductiveautomation.vision.api.client.AbstractClientModuleHook import org.imdc.extensions.common.DatasetExtensions import org.imdc.extensions.common.ExtensionDocProvider +import org.imdc.extensions.common.PyDatasetBuilder import org.imdc.extensions.common.UtilitiesExtensions import org.imdc.extensions.common.addPropertyBundle import org.imdc.extensions.common.expressions.IsAvailableFunction @@ -24,6 +25,12 @@ class ClientHook : AbstractClientModuleHook() { addPropertyBundle() addPropertyBundle() } + + PyDatasetBuilder.register() + } + + override fun shutdown() { + PyDatasetBuilder.unregister() } override fun initializeScriptManager(manager: ScriptManager) { diff --git a/common/src/main/kotlin/org/imdc/extensions/common/DatasetExtensions.kt b/common/src/main/kotlin/org/imdc/extensions/common/DatasetExtensions.kt index f1bb64d..b0b9095 100644 --- a/common/src/main/kotlin/org/imdc/extensions/common/DatasetExtensions.kt +++ b/common/src/main/kotlin/org/imdc/extensions/common/DatasetExtensions.kt @@ -7,6 +7,7 @@ import com.inductiveautomation.ignition.common.script.builtin.KeywordArgs import com.inductiveautomation.ignition.common.script.hints.ScriptArg import com.inductiveautomation.ignition.common.script.hints.ScriptFunction import com.inductiveautomation.ignition.common.util.DatasetBuilder +import com.inductiveautomation.ignition.common.xmlserialization.ClassNameResolver import org.apache.poi.ss.usermodel.CellType.BOOLEAN import org.apache.poi.ss.usermodel.CellType.FORMULA import org.apache.poi.ss.usermodel.CellType.NUMERIC @@ -14,6 +15,7 @@ import org.apache.poi.ss.usermodel.CellType.STRING import org.apache.poi.ss.usermodel.DateUtil import org.apache.poi.ss.usermodel.WorkbookFactory import org.python.core.Py +import org.python.core.PyBaseString import org.python.core.PyBoolean import org.python.core.PyFloat import org.python.core.PyFunction @@ -413,6 +415,8 @@ object DatasetExtensions { } } + private val classNameResolver = ClassNameResolver.createBasic() + @ScriptFunction(docBundlePrefix = "DatasetExtensions") @KeywordArgs( names = ["**columns"], @@ -422,10 +426,9 @@ object DatasetExtensions { if (args.size != keywords.size) throw Py.ValueError("builder must be called with only keyword arguments") val colNames = keywords.toList() val colTypes = args.mapIndexed { i, type -> - if (type !is PyType) { - throw Py.TypeError("${keywords[i]} was a ${type::class.simpleName}, but should be a type") - } when (type) { + is PyBaseString -> classNameResolver.classForName(type.asString()) + !is PyType -> throw Py.TypeError("${keywords[i]} was a ${type::class.simpleName}, but should be a type") PyString.TYPE, PyUnicode.TYPE -> String::class.java PyBoolean.TYPE -> Boolean::class.java PyInteger.TYPE -> Int::class.java diff --git a/common/src/main/kotlin/org/imdc/extensions/common/PyDatasetBuilder.kt b/common/src/main/kotlin/org/imdc/extensions/common/PyDatasetBuilder.kt new file mode 100644 index 0000000..66cd4ba --- /dev/null +++ b/common/src/main/kotlin/org/imdc/extensions/common/PyDatasetBuilder.kt @@ -0,0 +1,57 @@ +package org.imdc.extensions.common + +import com.inductiveautomation.ignition.common.Dataset +import com.inductiveautomation.ignition.common.script.DisposablePyObjectAdapter +import com.inductiveautomation.ignition.common.util.DatasetBuilder +import com.inductiveautomation.ignition.common.xmlserialization.ClassNameResolver +import org.python.core.Py +import org.python.core.PyObject +import org.python.core.adapter.PyObjectAdapter + +@Suppress("unused") +class PyDatasetBuilder(private val builder: DatasetBuilder) : PyObject() { + private val resolver = ClassNameResolver.createBasic() + + fun colNames(vararg names: String) = apply { + builder.colNames(names.toList()) + } + + fun colNames(names: List) = apply { + builder.colNames(names) + } + + fun colTypes(vararg types: String) = apply { + builder.colTypes(types.map { resolver.classForName(it) }) + } + + fun colTypes(vararg types: Class<*>?) = apply { + builder.colTypes(types.toList()) + } + + fun colTypes(types: List>) = apply { + builder.colTypes(types) + } + + fun addRow(vararg values: Any?) = apply { + builder.addRow(*values) + } + + fun build(): Dataset = builder.build() + + companion object { + private val adapter = DisposablePyObjectAdapter(DatasetBuilderAdapter()) + + fun register() { + Py.getAdapter().addPostClass(adapter) + } + + fun unregister() { + adapter.dispose() + } + } +} + +class DatasetBuilderAdapter : PyObjectAdapter { + override fun adapt(o: Any?): PyObject = PyDatasetBuilder(o as DatasetBuilder) + override fun canAdapt(o: Any?): Boolean = o is DatasetBuilder +} diff --git a/common/src/test/kotlin/org/imdc/extensions/common/DatasetExtensionsTests.kt b/common/src/test/kotlin/org/imdc/extensions/common/DatasetExtensionsTests.kt index 4cf11aa..1098f4b 100644 --- a/common/src/test/kotlin/org/imdc/extensions/common/DatasetExtensionsTests.kt +++ b/common/src/test/kotlin/org/imdc/extensions/common/DatasetExtensionsTests.kt @@ -58,6 +58,8 @@ class DatasetExtensionsTests : JythonTest( } init { + PyDatasetBuilder.register() + context("Map tests") { test("Null dataset") { shouldThrowPyException(Py.TypeError) { @@ -313,6 +315,23 @@ class DatasetExtensionsTests : JythonTest( } } + test("String type codes") { + eval("utils.builder(a='i', b='str', c='b').addRow(1, '2', False).build()").asClue { + it.rowCount shouldBe 1 + it.columnCount shouldBe 3 + it.columnNames shouldBe listOf( + "a", + "b", + "c", + ) + it.columnTypes shouldBe listOf( + Int::class.java, + String::class.java, + Boolean::class.java, + ) + } + } + test("Complex types") { eval( """ diff --git a/designer/src/main/kotlin/org/imdc/extensions/designer/DesignerHook.kt b/designer/src/main/kotlin/org/imdc/extensions/designer/DesignerHook.kt index 5fed403..2ecf426 100644 --- a/designer/src/main/kotlin/org/imdc/extensions/designer/DesignerHook.kt +++ b/designer/src/main/kotlin/org/imdc/extensions/designer/DesignerHook.kt @@ -8,6 +8,7 @@ import com.inductiveautomation.ignition.designer.model.AbstractDesignerModuleHoo import com.inductiveautomation.ignition.designer.model.DesignerContext import org.imdc.extensions.common.DatasetExtensions import org.imdc.extensions.common.ExtensionDocProvider +import org.imdc.extensions.common.PyDatasetBuilder import org.imdc.extensions.common.UtilitiesExtensions import org.imdc.extensions.common.addPropertyBundle import org.imdc.extensions.common.expressions.IsAvailableFunction @@ -24,6 +25,12 @@ class DesignerHook : AbstractDesignerModuleHook() { addPropertyBundle() addPropertyBundle() } + + PyDatasetBuilder.register() + } + + override fun shutdown() { + PyDatasetBuilder.unregister() } override fun initializeScriptManager(manager: ScriptManager) { diff --git a/gateway/src/main/kotlin/org/imdc/extensions/gateway/GatewayHook.kt b/gateway/src/main/kotlin/org/imdc/extensions/gateway/GatewayHook.kt index 38af2ea..e4d9b4b 100644 --- a/gateway/src/main/kotlin/org/imdc/extensions/gateway/GatewayHook.kt +++ b/gateway/src/main/kotlin/org/imdc/extensions/gateway/GatewayHook.kt @@ -8,6 +8,7 @@ import com.inductiveautomation.ignition.gateway.model.AbstractGatewayModuleHook import com.inductiveautomation.ignition.gateway.model.GatewayContext import org.imdc.extensions.common.DatasetExtensions import org.imdc.extensions.common.ExtensionDocProvider +import org.imdc.extensions.common.PyDatasetBuilder import org.imdc.extensions.common.UtilitiesExtensions import org.imdc.extensions.common.addPropertyBundle import org.imdc.extensions.common.expressions.IsAvailableFunction @@ -24,10 +25,14 @@ class GatewayHook : AbstractGatewayModuleHook() { addPropertyBundle() addPropertyBundle() } + + PyDatasetBuilder.register() } override fun startup(activationState: LicenseState) {} - override fun shutdown() {} + override fun shutdown() { + PyDatasetBuilder.unregister() + } override fun initializeScriptManager(manager: ScriptManager) { manager.apply { From bbe233542610fc9111bba7a132042d6d65137194 Mon Sep 17 00:00:00 2001 From: Paul Griffith Date: Tue, 22 Nov 2022 15:31:18 -0800 Subject: [PATCH 5/5] Improve coverage and unify methods --- .../extensions/common/DatasetExtensions.kt | 24 +++--- .../extensions/common/PyDatasetBuilder.kt | 9 +-- .../common/DatasetExtensions.properties | 2 +- .../common/DatasetExtensionsTests.kt | 80 ++++++++++++++++++- 4 files changed, 98 insertions(+), 17 deletions(-) diff --git a/common/src/main/kotlin/org/imdc/extensions/common/DatasetExtensions.kt b/common/src/main/kotlin/org/imdc/extensions/common/DatasetExtensions.kt index b0b9095..8aaa091 100644 --- a/common/src/main/kotlin/org/imdc/extensions/common/DatasetExtensions.kt +++ b/common/src/main/kotlin/org/imdc/extensions/common/DatasetExtensions.kt @@ -426,17 +426,23 @@ object DatasetExtensions { if (args.size != keywords.size) throw Py.ValueError("builder must be called with only keyword arguments") val colNames = keywords.toList() val colTypes = args.mapIndexed { i, type -> - when (type) { - is PyBaseString -> classNameResolver.classForName(type.asString()) - !is PyType -> throw Py.TypeError("${keywords[i]} was a ${type::class.simpleName}, but should be a type") - PyString.TYPE, PyUnicode.TYPE -> String::class.java - PyBoolean.TYPE -> Boolean::class.java - PyInteger.TYPE -> Int::class.java - PyLong.TYPE -> Long::class.java - PyFloat.TYPE -> Double::class.java - else -> type.toJava>() + try { + type.asJavaClass() + } catch (e: ClassCastException) { + throw Py.TypeError("${keywords[i]} was a ${type::class.simpleName}, but should be a type or valid string typecode") } } return DatasetBuilder.newBuilder().colNames(colNames).colTypes(colTypes) } + + fun PyObject.asJavaClass(): Class<*>? = when (this) { + is PyBaseString -> classNameResolver.classForName(asString()) + !is PyType -> throw ClassCastException() + PyString.TYPE, PyUnicode.TYPE -> String::class.java + PyBoolean.TYPE -> Boolean::class.java + PyInteger.TYPE -> Int::class.java + PyLong.TYPE -> Long::class.java + PyFloat.TYPE -> Double::class.java + else -> toJava>() + } } diff --git a/common/src/main/kotlin/org/imdc/extensions/common/PyDatasetBuilder.kt b/common/src/main/kotlin/org/imdc/extensions/common/PyDatasetBuilder.kt index 66cd4ba..2d98097 100644 --- a/common/src/main/kotlin/org/imdc/extensions/common/PyDatasetBuilder.kt +++ b/common/src/main/kotlin/org/imdc/extensions/common/PyDatasetBuilder.kt @@ -4,6 +4,7 @@ import com.inductiveautomation.ignition.common.Dataset import com.inductiveautomation.ignition.common.script.DisposablePyObjectAdapter import com.inductiveautomation.ignition.common.util.DatasetBuilder import com.inductiveautomation.ignition.common.xmlserialization.ClassNameResolver +import org.imdc.extensions.common.DatasetExtensions.asJavaClass import org.python.core.Py import org.python.core.PyObject import org.python.core.adapter.PyObjectAdapter @@ -20,12 +21,8 @@ class PyDatasetBuilder(private val builder: DatasetBuilder) : PyObject() { builder.colNames(names) } - fun colTypes(vararg types: String) = apply { - builder.colTypes(types.map { resolver.classForName(it) }) - } - - fun colTypes(vararg types: Class<*>?) = apply { - builder.colTypes(types.toList()) + fun colTypes(vararg types: PyObject) = apply { + builder.colTypes(types.map { it.asJavaClass() }) } fun colTypes(types: List>) = apply { diff --git a/common/src/main/resources/org/imdc/extensions/common/DatasetExtensions.properties b/common/src/main/resources/org/imdc/extensions/common/DatasetExtensions.properties index 9297d14..990ae7d 100644 --- a/common/src/main/resources/org/imdc/extensions/common/DatasetExtensions.properties +++ b/common/src/main/resources/org/imdc/extensions/common/DatasetExtensions.properties @@ -43,6 +43,6 @@ columnsEqual.param.includeTypes=Pass True if the column types must match as well columnsEqual.returns=True if the two datasets have the same columns, per additional parameters. builder.desc=Creates a new dataset using supplied column names and types. -builder.param.**columns=Optional. Keyword arguments can be supplied to predefine column names and types. The value of the argument should be a Java or Python type. +builder.param.**columns=Optional. Keyword arguments can be supplied to predefine column names and types. The value of the argument should be string "typecode" (see system.dataset.fromCSV) or a Java or Python class instance. builder.returns=A DatasetBuilder object. Use addRow(value, ...) to add new values, and build() to construct the final dataset. \ If keyword arguments were not supplied, column names and types can be manually declared using colNames() and colTypes(). diff --git a/common/src/test/kotlin/org/imdc/extensions/common/DatasetExtensionsTests.kt b/common/src/test/kotlin/org/imdc/extensions/common/DatasetExtensionsTests.kt index 1098f4b..0c7d07f 100644 --- a/common/src/test/kotlin/org/imdc/extensions/common/DatasetExtensionsTests.kt +++ b/common/src/test/kotlin/org/imdc/extensions/common/DatasetExtensionsTests.kt @@ -43,6 +43,9 @@ class DatasetExtensionsTests : JythonTest( globals["date"] = Date::class.java globals["color"] = Color::class.java + globals["javaInt"] = Int::class.java + globals["javaString"] = String::class.java + globals["javaBool"] = Boolean::class.java }, ) { private fun Dataset.asClue(assertions: (Dataset) -> Unit) { @@ -315,7 +318,7 @@ class DatasetExtensionsTests : JythonTest( } } - test("String type codes") { + test("String type codes in builder call") { eval("utils.builder(a='i', b='str', c='b').addRow(1, '2', False).build()").asClue { it.rowCount shouldBe 1 it.columnCount shouldBe 3 @@ -332,6 +335,81 @@ class DatasetExtensionsTests : JythonTest( } } + test("Separate colTypes as java types") { + eval( + """ + utils.builder() \ + .colNames('a', 'b', 'c') \ + .colTypes(javaInt, javaString, javaBool) \ + .addRow(1, '2', False) \ + .build() + """.trimIndent(), + ).asClue { + it.rowCount shouldBe 1 + it.columnCount shouldBe 3 + it.columnNames shouldBe listOf( + "a", + "b", + "c", + ) + it.columnTypes shouldBe listOf( + Int::class.java, + String::class.java, + Boolean::class.java, + ) + } + } + + test("Separate colTypes as Python types") { + eval( + """ + utils.builder() \ + .colNames('a', 'b', 'c') \ + .colTypes(int, str, bool) \ + .addRow(1, '2', False) \ + .build() + """.trimIndent(), + ).asClue { + it.rowCount shouldBe 1 + it.columnCount shouldBe 3 + it.columnNames shouldBe listOf( + "a", + "b", + "c", + ) + it.columnTypes shouldBe listOf( + Int::class.java, + String::class.java, + Boolean::class.java, + ) + } + } + + test("Separate colTypes as string shortcodes") { + eval( + """ + utils.builder() \ + .colNames('a', 'b', 'c') \ + .colTypes('i', 'str', 'b') \ + .addRow(1, '2', False) \ + .build() + """.trimIndent(), + ).asClue { + it.rowCount shouldBe 1 + it.columnCount shouldBe 3 + it.columnNames shouldBe listOf( + "a", + "b", + "c", + ) + it.columnTypes shouldBe listOf( + Int::class.java, + String::class.java, + Boolean::class.java, + ) + } + } + test("Complex types") { eval( """