diff --git a/gradle.properties b/gradle.properties index 87e8c31b6..ac4074041 100644 --- a/gradle.properties +++ b/gradle.properties @@ -28,6 +28,6 @@ org.gradle.jvmargs=-XX:MaxMetaspaceSize=1024m -Xmx2048m # Turn off README check when running check task skipReadmeCheck=false -jupyterApiVersion=0.9.1-42 +jupyterApiVersion=0.9.1-45 kotlin.jupyter.add.api=false kotlin.jupyter.add.scanner=false diff --git a/jupyter-lib/api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/api/graphs/GraphNode.kt b/jupyter-lib/api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/api/graphs/GraphNode.kt new file mode 100644 index 000000000..d450acb1c --- /dev/null +++ b/jupyter-lib/api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/api/graphs/GraphNode.kt @@ -0,0 +1,38 @@ +package org.jetbrains.kotlinx.jupyter.api.graphs + +/** + * Graph node which represents the object as a part of some hierarchy + * + * Classes implementing this interface should take care of [equals] and [hashCode] + * because they are used for testing the nodes for equality, and wrong implementation + * of these methods may lead to the wrong graph rendering, StackOverflow / OutOfMemory + * errors and so on. See example in [NodeWrapper] + * + * @param T Underlying object type + */ +interface GraphNode { + /** + * Node label with all required information + */ + val label: Label + + /** + * Nodes which are connected with the ingoing edges to this one: + * {this} <- {inNode} + */ + val inNodes: List> + + /** + * Nodes which are connected with the outgoing edges to this one: + * {this} -> {outNode} + */ + val outNodes: List> + + /** + * Nodes which are connected with the undirected edges to this one: + * {this} -- {biNode} + */ + val biNodes: List> + + companion object +} diff --git a/jupyter-lib/api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/api/graphs/Label.kt b/jupyter-lib/api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/api/graphs/Label.kt new file mode 100644 index 000000000..1b335e932 --- /dev/null +++ b/jupyter-lib/api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/api/graphs/Label.kt @@ -0,0 +1,17 @@ +package org.jetbrains.kotlinx.jupyter.api.graphs + +/** + * [Label] contains all information related to the node itself + */ +interface Label { + /** + * Node text. May be simple simple text or HTML + */ + val text: String + + /** + * Shape of this node. The full list of shapes is given + * [here](https://graphviz.org/doc/info/shapes.html) + */ + val shape: String? get() = null +} diff --git a/jupyter-lib/api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/api/graphs/NodeWrapper.kt b/jupyter-lib/api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/api/graphs/NodeWrapper.kt new file mode 100644 index 000000000..09f0fc987 --- /dev/null +++ b/jupyter-lib/api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/api/graphs/NodeWrapper.kt @@ -0,0 +1,22 @@ +package org.jetbrains.kotlinx.jupyter.api.graphs + +import org.jetbrains.kotlinx.jupyter.api.graphs.labels.TextLabel + +/** + * Use [NodeWrapper] if [T] cannot implement [GraphNode] itself for some reason + */ +abstract class NodeWrapper(val value: T) : GraphNode { + override val label: Label get() = TextLabel(value.toString()) + + override val inNodes get() = listOf>() + override val outNodes get() = listOf>() + override val biNodes get() = listOf>() + + override fun equals(other: Any?): Boolean { + return other is NodeWrapper<*> && other.value == this.value + } + + override fun hashCode(): Int { + return value.hashCode() + } +} diff --git a/jupyter-lib/api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/api/graphs/labels/FilteringPropObjectLabel.kt b/jupyter-lib/api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/api/graphs/labels/FilteringPropObjectLabel.kt new file mode 100644 index 000000000..284c6b6ec --- /dev/null +++ b/jupyter-lib/api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/api/graphs/labels/FilteringPropObjectLabel.kt @@ -0,0 +1,17 @@ +package org.jetbrains.kotlinx.jupyter.api.graphs.labels + +import kotlin.reflect.KProperty1 + +/** + * Convenience class for creating [PropObjectLabel] if only fixed subset + * of properties [propertiesToRender] should be rendered + */ +class FilteringPropObjectLabel( + value: T, + override val mainText: String = value.toString(), + private val propertiesToRender: Collection = emptyList(), +) : PropObjectLabel(value) { + override fun shouldRenderProperty(prop: KProperty1): Boolean { + return prop.name in propertiesToRender + } +} diff --git a/jupyter-lib/api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/api/graphs/labels/KClassLabel.kt b/jupyter-lib/api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/api/graphs/labels/KClassLabel.kt new file mode 100644 index 000000000..e69ce9b06 --- /dev/null +++ b/jupyter-lib/api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/api/graphs/labels/KClassLabel.kt @@ -0,0 +1,13 @@ +package org.jetbrains.kotlinx.jupyter.api.graphs.labels + +import kotlin.reflect.KClass + +/** + * Label representing [kClass] with all members in HTML table + */ +class KClassLabel(private val kClass: KClass<*>) : RecordTableLabel() { + override val mainText get() = kClass.simpleName.toString() + + override val properties: Collection> + get() = kClass.members.map { listOf(it.name, it.returnType.toString()) } +} diff --git a/jupyter-lib/api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/api/graphs/labels/PropObjectLabel.kt b/jupyter-lib/api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/api/graphs/labels/PropObjectLabel.kt new file mode 100644 index 000000000..4f654f951 --- /dev/null +++ b/jupyter-lib/api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/api/graphs/labels/PropObjectLabel.kt @@ -0,0 +1,28 @@ +package org.jetbrains.kotlinx.jupyter.api.graphs.labels + +import kotlin.reflect.KProperty1 +import kotlin.reflect.full.memberProperties +import kotlin.reflect.jvm.isAccessible + +/** + * Renders [value] object with its properties for + * those [shouldRenderProperty] returns `true` + */ +open class PropObjectLabel(val value: T) : RecordTableLabel() { + override val mainText get() = value.toString() + + override val properties: Collection> get() { + val kClass = value::class + + return kClass.memberProperties + .filter(::shouldRenderProperty) + .map { prop -> + @Suppress("UNCHECKED_CAST") + prop as KProperty1 + prop.isAccessible = true + listOf(prop.name, prop.invoke(value).toString()) + } + } + + open fun shouldRenderProperty(prop: KProperty1) = true +} diff --git a/jupyter-lib/api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/api/graphs/labels/RecordTableLabel.kt b/jupyter-lib/api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/api/graphs/labels/RecordTableLabel.kt new file mode 100644 index 000000000..5c9693e4a --- /dev/null +++ b/jupyter-lib/api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/api/graphs/labels/RecordTableLabel.kt @@ -0,0 +1,42 @@ +package org.jetbrains.kotlinx.jupyter.api.graphs.labels + +import org.jetbrains.kotlinx.jupyter.api.graphs.Label + +/** + * Renders as n-column table + * First column consists of one cell containing [mainText]. + * Next `(n-1)` columns contain values from [properties]. It is + * supposed that all element in [properties] collection would + * have `(n-1)` elements. + */ +abstract class RecordTableLabel : Label { + override val text: String get() { + val nProperties = properties.size + + fun inTable(builderAction: StringBuilder.() -> Unit) = buildString { + append("<") + builderAction() + append("
>") + } + + if (nProperties == 0) return inTable { append("$mainText") } + return inTable { + properties.forEachIndexed { i, prop -> + append("") + if (i == 0 && mainText != null) { + append("""$mainText""") + } + prop.forEach { value -> + append("""$value""") + } + appendLine("") + } + } + } + + override val shape: String? get() = "plaintext" + + abstract val mainText: String? + + abstract val properties: Collection> +} diff --git a/jupyter-lib/api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/api/graphs/labels/TextLabel.kt b/jupyter-lib/api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/api/graphs/labels/TextLabel.kt new file mode 100644 index 000000000..8c09ecd39 --- /dev/null +++ b/jupyter-lib/api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/api/graphs/labels/TextLabel.kt @@ -0,0 +1,10 @@ +package org.jetbrains.kotlinx.jupyter.api.graphs.labels + +import org.jetbrains.kotlinx.jupyter.api.graphs.Label + +/** + * Label representing a plain text inside a given [shape] + */ +class TextLabel(value: String, override val shape: String? = "ellipse") : Label { + override val text: String = "\"${value.replace("\"", "\\\"")}\"" +} diff --git a/jupyter-lib/lib-ext/build.gradle.kts b/jupyter-lib/lib-ext/build.gradle.kts index d1de2953f..bcea53845 100644 --- a/jupyter-lib/lib-ext/build.gradle.kts +++ b/jupyter-lib/lib-ext/build.gradle.kts @@ -32,6 +32,8 @@ dependencies { implementation("org.apache.xmlgraphics:fop:2.6") implementation("org.apache.xmlgraphics:batik-codec:1.14") implementation("org.apache.xmlgraphics:xmlgraphics-commons:2.6") + + implementation("guru.nidi:graphviz-java:0.18.1") } tasks.test { diff --git a/jupyter-lib/lib-ext/src/main/kotlin/org/jetbrains/kotlinx/jupyter/ext/graph/structure/DirectedEdge.kt b/jupyter-lib/lib-ext/src/main/kotlin/org/jetbrains/kotlinx/jupyter/ext/graph/structure/DirectedEdge.kt new file mode 100644 index 000000000..4417a4da4 --- /dev/null +++ b/jupyter-lib/lib-ext/src/main/kotlin/org/jetbrains/kotlinx/jupyter/ext/graph/structure/DirectedEdge.kt @@ -0,0 +1,8 @@ +package org.jetbrains.kotlinx.jupyter.ext.graph.structure + +import org.jetbrains.kotlinx.jupyter.api.graphs.GraphNode + +data class DirectedEdge( + val fromNode: GraphNode, + val toNode: GraphNode, +) diff --git a/jupyter-lib/lib-ext/src/main/kotlin/org/jetbrains/kotlinx/jupyter/ext/graph/structure/Graph.kt b/jupyter-lib/lib-ext/src/main/kotlin/org/jetbrains/kotlinx/jupyter/ext/graph/structure/Graph.kt new file mode 100644 index 000000000..76e83f6e5 --- /dev/null +++ b/jupyter-lib/lib-ext/src/main/kotlin/org/jetbrains/kotlinx/jupyter/ext/graph/structure/Graph.kt @@ -0,0 +1,22 @@ +package org.jetbrains.kotlinx.jupyter.ext.graph.structure + +import org.jetbrains.kotlinx.jupyter.api.graphs.GraphNode + +interface Graph : MultiGraph { + override val directedEdges: Set> + override val undirectedEdges: Set> + + companion object { + fun of(elements: Iterable>): Graph { + val nodes = mutableSetOf>() + val directedEdges = mutableSetOf>() + val undirectedEdges = mutableSetOf>() + + for (element in elements) element.populate(nodes, directedEdges, undirectedEdges) + + return GraphImpl(nodes, directedEdges, undirectedEdges) + } + + fun of(vararg elements: GraphNode): Graph = of(elements.toList()) + } +} diff --git a/jupyter-lib/lib-ext/src/main/kotlin/org/jetbrains/kotlinx/jupyter/ext/graph/structure/GraphImpl.kt b/jupyter-lib/lib-ext/src/main/kotlin/org/jetbrains/kotlinx/jupyter/ext/graph/structure/GraphImpl.kt new file mode 100644 index 000000000..5caf86d09 --- /dev/null +++ b/jupyter-lib/lib-ext/src/main/kotlin/org/jetbrains/kotlinx/jupyter/ext/graph/structure/GraphImpl.kt @@ -0,0 +1,9 @@ +package org.jetbrains.kotlinx.jupyter.ext.graph.structure + +import org.jetbrains.kotlinx.jupyter.api.graphs.GraphNode + +class GraphImpl( + override val nodes: Set>, + override val directedEdges: Set>, + override val undirectedEdges: Set>, +) : Graph diff --git a/jupyter-lib/lib-ext/src/main/kotlin/org/jetbrains/kotlinx/jupyter/ext/graph/structure/MultiGraph.kt b/jupyter-lib/lib-ext/src/main/kotlin/org/jetbrains/kotlinx/jupyter/ext/graph/structure/MultiGraph.kt new file mode 100644 index 000000000..1b770de49 --- /dev/null +++ b/jupyter-lib/lib-ext/src/main/kotlin/org/jetbrains/kotlinx/jupyter/ext/graph/structure/MultiGraph.kt @@ -0,0 +1,23 @@ +package org.jetbrains.kotlinx.jupyter.ext.graph.structure + +import org.jetbrains.kotlinx.jupyter.api.graphs.GraphNode + +interface MultiGraph { + val nodes: Set> + val directedEdges: Collection> + val undirectedEdges: Collection> + + companion object { + fun of(elements: Iterable>): MultiGraph { + val nodes = mutableSetOf>() + val directedEdges = mutableListOf>() + val undirectedEdges = mutableListOf>() + + for (element in elements) element.populate(nodes, directedEdges, undirectedEdges) + + return MultiGraphImpl(nodes, directedEdges, undirectedEdges) + } + + fun of(vararg elements: GraphNode): MultiGraph = of(elements.toList()) + } +} diff --git a/jupyter-lib/lib-ext/src/main/kotlin/org/jetbrains/kotlinx/jupyter/ext/graph/structure/MultiGraphImpl.kt b/jupyter-lib/lib-ext/src/main/kotlin/org/jetbrains/kotlinx/jupyter/ext/graph/structure/MultiGraphImpl.kt new file mode 100644 index 000000000..186cdb342 --- /dev/null +++ b/jupyter-lib/lib-ext/src/main/kotlin/org/jetbrains/kotlinx/jupyter/ext/graph/structure/MultiGraphImpl.kt @@ -0,0 +1,9 @@ +package org.jetbrains.kotlinx.jupyter.ext.graph.structure + +import org.jetbrains.kotlinx.jupyter.api.graphs.GraphNode + +class MultiGraphImpl( + override val nodes: Set>, + override val directedEdges: List>, + override val undirectedEdges: List>, +) : MultiGraph diff --git a/jupyter-lib/lib-ext/src/main/kotlin/org/jetbrains/kotlinx/jupyter/ext/graph/structure/UndirectedEdge.kt b/jupyter-lib/lib-ext/src/main/kotlin/org/jetbrains/kotlinx/jupyter/ext/graph/structure/UndirectedEdge.kt new file mode 100644 index 000000000..4ec873f86 --- /dev/null +++ b/jupyter-lib/lib-ext/src/main/kotlin/org/jetbrains/kotlinx/jupyter/ext/graph/structure/UndirectedEdge.kt @@ -0,0 +1,22 @@ +package org.jetbrains.kotlinx.jupyter.ext.graph.structure + +import org.jetbrains.kotlinx.jupyter.api.graphs.GraphNode + +data class UndirectedEdge( + val fromNode: GraphNode, + val toNode: GraphNode, +) { + override fun equals(other: Any?): Boolean { + return other is UndirectedEdge<*> && ( + (fromNode == other.fromNode) && (toNode == other.toNode) || + (fromNode == other.toNode) && (toNode == other.fromNode) + ) + } + + override fun hashCode(): Int { + var h1 = fromNode.hashCode() + var h2 = toNode.hashCode() + if (h1 > h2) { val t = h2; h2 = h1; h1 = t } + return 31 * h1 + h2 + } +} diff --git a/jupyter-lib/lib-ext/src/main/kotlin/org/jetbrains/kotlinx/jupyter/ext/graph/structure/util.kt b/jupyter-lib/lib-ext/src/main/kotlin/org/jetbrains/kotlinx/jupyter/ext/graph/structure/util.kt new file mode 100644 index 000000000..d2cb805ad --- /dev/null +++ b/jupyter-lib/lib-ext/src/main/kotlin/org/jetbrains/kotlinx/jupyter/ext/graph/structure/util.kt @@ -0,0 +1,51 @@ +package org.jetbrains.kotlinx.jupyter.ext.graph.structure + +import org.jetbrains.kotlinx.jupyter.api.graphs.GraphNode + +val GraphNode.allParents: Iterable> get() { + return IterablesView(listOf(inNodes, outNodes, biNodes)) +} + +private class IterablesView(private val iterables: Iterable>) : Iterable { + override fun iterator(): Iterator { + return MyIterator(iterables) + } + + class MyIterator(iterables: Iterable>) : Iterator { + private val outerIterator = iterables.iterator() + private var innerIterator: Iterator? = null + + override fun hasNext(): Boolean { + while (innerIterator?.hasNext() != true) { + if (!outerIterator.hasNext()) return false + innerIterator = outerIterator.next().iterator() + } + return true + } + + override fun next(): T { + if (!hasNext()) throw IndexOutOfBoundsException() + return innerIterator!!.next() + } + } +} + +fun GraphNode.populate( + nodes: MutableSet>, + directedEdges: MutableCollection>, + undirectedEdges: MutableCollection>, +) { + nodes.add(this) + for (parent in inNodes) { + directedEdges.add(DirectedEdge(parent, this)) + } + for (parent in outNodes) { + directedEdges.add(DirectedEdge(this, parent)) + } + for (parent in this.biNodes) { + undirectedEdges.add(UndirectedEdge(this, parent)) + } + for (parent in allParents) { + if (parent !in nodes) parent.populate(nodes, directedEdges, undirectedEdges) + } +} diff --git a/jupyter-lib/lib-ext/src/main/kotlin/org/jetbrains/kotlinx/jupyter/ext/graph/visualization/graphViz.kt b/jupyter-lib/lib-ext/src/main/kotlin/org/jetbrains/kotlinx/jupyter/ext/graph/visualization/graphViz.kt new file mode 100644 index 000000000..349f781c4 --- /dev/null +++ b/jupyter-lib/lib-ext/src/main/kotlin/org/jetbrains/kotlinx/jupyter/ext/graph/visualization/graphViz.kt @@ -0,0 +1,54 @@ +package org.jetbrains.kotlinx.jupyter.ext.graph.visualization + +import guru.nidi.graphviz.engine.Engine +import guru.nidi.graphviz.engine.Format +import guru.nidi.graphviz.engine.Graphviz +import guru.nidi.graphviz.parse.Parser +import org.jetbrains.kotlinx.jupyter.api.graphs.GraphNode +import org.jetbrains.kotlinx.jupyter.ext.Image +import org.jetbrains.kotlinx.jupyter.ext.graph.structure.MultiGraph +import java.io.ByteArrayOutputStream + +fun MultiGraph.dotText(): String { + val nodesNumbers = nodes.mapIndexed { index, hierarchyElement -> hierarchyElement to index }.toMap() + fun id(el: GraphNode) = "n${nodesNumbers[el]}" + + return buildString { + appendLine("""digraph "" { """) + for (node in nodes) { + val nodeId = id(node) + appendLine("$nodeId ;") + append("$nodeId [") + with(node.label) { + append("label=$text ") + shape?.let { append("shape=$it ") } + } + appendLine("] ;") + } + + for ((n1, n2) in directedEdges) { + appendLine("${id(n1)} -> ${id(n2)} ;") + } + for ((n1, n2) in undirectedEdges) { + appendLine("${id(n1)} -> ${id(n2)} [dir=none] ;") + } + appendLine("}") + } +} + +fun renderDotText(text: String): Image { + val graph = Parser().read(text) + val stream = ByteArrayOutputStream() + Graphviz + .fromGraph(graph) + .engine(Engine.DOT) + .render(Format.SVG) + .toOutputStream(stream) + return Image(stream.toByteArray(), "svg") +} + +fun MultiGraph.render(): Image { + return renderDotText(dotText()) +} + +fun MultiGraph.toHTML() = render().toHTML() diff --git a/jupyter-lib/lib-ext/src/main/kotlin/org/jetbrains/kotlinx/jupyter/ext/graph/wrappers/ClassLoaderNode.kt b/jupyter-lib/lib-ext/src/main/kotlin/org/jetbrains/kotlinx/jupyter/ext/graph/wrappers/ClassLoaderNode.kt new file mode 100644 index 000000000..f9d7930b5 --- /dev/null +++ b/jupyter-lib/lib-ext/src/main/kotlin/org/jetbrains/kotlinx/jupyter/ext/graph/wrappers/ClassLoaderNode.kt @@ -0,0 +1,25 @@ +package org.jetbrains.kotlinx.jupyter.ext.graph.wrappers + +import org.jetbrains.kotlinx.jupyter.api.graphs.GraphNode +import org.jetbrains.kotlinx.jupyter.api.graphs.NodeWrapper +import org.jetbrains.kotlinx.jupyter.api.graphs.labels.TextLabel +import java.net.URLClassLoader +import kotlin.reflect.KClass + +class ClassLoaderNode(node: ClassLoader) : NodeWrapper(node) { + override val inNodes by lazy { + node.parent?.let { listOf(ClassLoaderNode(it)) } ?: emptyList() + } + override val label = TextLabel( + when (node) { + is URLClassLoader -> node.urLs.joinToString("\\n", "URL ClassLoader:\\n") { + it.toString() + } + else -> node.toString() + } + ) +} + +fun GraphNode.Companion.fromClassLoader(classLoader: ClassLoader) = ClassLoaderNode(classLoader) +fun GraphNode.Companion.fromClassLoader(kClass: KClass<*>) = fromClassLoader(kClass.java.classLoader) +inline fun GraphNode.Companion.fromClassLoader() = fromClassLoader(T::class) diff --git a/jupyter-lib/lib-ext/src/main/kotlin/org/jetbrains/kotlinx/jupyter/ext/graph/wrappers/KClassNode.kt b/jupyter-lib/lib-ext/src/main/kotlin/org/jetbrains/kotlinx/jupyter/ext/graph/wrappers/KClassNode.kt new file mode 100644 index 000000000..752321d3b --- /dev/null +++ b/jupyter-lib/lib-ext/src/main/kotlin/org/jetbrains/kotlinx/jupyter/ext/graph/wrappers/KClassNode.kt @@ -0,0 +1,19 @@ +package org.jetbrains.kotlinx.jupyter.ext.graph.wrappers + +import org.jetbrains.kotlinx.jupyter.api.graphs.GraphNode +import org.jetbrains.kotlinx.jupyter.api.graphs.Label +import org.jetbrains.kotlinx.jupyter.api.graphs.NodeWrapper +import org.jetbrains.kotlinx.jupyter.api.graphs.labels.KClassLabel +import kotlin.reflect.KClass +import kotlin.reflect.full.superclasses + +class KClassNode(node: KClass<*>) : NodeWrapper>(node) { + override val label: Label get() = KClassLabel(value) + + override val inNodes by lazy { + node.superclasses.map { KClassNode(it) } + } +} + +fun GraphNode.Companion.fromClass(kClass: KClass<*>) = KClassNode(kClass) +inline fun GraphNode.Companion.fromClass() = fromClass(T::class) diff --git a/jupyter-lib/lib-ext/src/main/kotlin/org/jetbrains/kotlinx/jupyter/ext/integration/Integration.kt b/jupyter-lib/lib-ext/src/main/kotlin/org/jetbrains/kotlinx/jupyter/ext/integration/Integration.kt index 26bddf613..1490f1594 100644 --- a/jupyter-lib/lib-ext/src/main/kotlin/org/jetbrains/kotlinx/jupyter/ext/integration/Integration.kt +++ b/jupyter-lib/lib-ext/src/main/kotlin/org/jetbrains/kotlinx/jupyter/ext/integration/Integration.kt @@ -1,11 +1,25 @@ package org.jetbrains.kotlinx.jupyter.ext.integration import org.jetbrains.kotlinx.jupyter.api.annotations.JupyterLibrary +import org.jetbrains.kotlinx.jupyter.api.graphs.GraphNode import org.jetbrains.kotlinx.jupyter.api.libraries.JupyterIntegration +import org.jetbrains.kotlinx.jupyter.ext.graph.structure.Graph +import org.jetbrains.kotlinx.jupyter.ext.graph.structure.MultiGraph +import org.jetbrains.kotlinx.jupyter.ext.graph.visualization.render +import org.jetbrains.kotlinx.jupyter.ext.graph.wrappers.KClassNode @JupyterLibrary class Integration : JupyterIntegration() { override fun Builder.onLoaded() { import("org.jetbrains.kotlinx.jupyter.ext.*") + importPackage>() + importPackage() + + render> { + it.render() + } + renderWithHost> { host, value -> + notebook.renderersProcessor.renderValue(host, Graph.of(value)) ?: "null" + } } } diff --git a/jupyter-lib/lib-ext/src/test/kotlin/org/jetbrains/kotlinx/jupyter/ext/RenderingTests.kt b/jupyter-lib/lib-ext/src/test/kotlin/org/jetbrains/kotlinx/jupyter/ext/RenderingTests.kt index 5aeffd5f1..ea5edaf2c 100644 --- a/jupyter-lib/lib-ext/src/test/kotlin/org/jetbrains/kotlinx/jupyter/ext/RenderingTests.kt +++ b/jupyter-lib/lib-ext/src/test/kotlin/org/jetbrains/kotlinx/jupyter/ext/RenderingTests.kt @@ -1,5 +1,10 @@ package org.jetbrains.kotlinx.jupyter.ext +import org.jetbrains.kotlinx.jupyter.api.graphs.GraphNode +import org.jetbrains.kotlinx.jupyter.ext.graph.structure.Graph +import org.jetbrains.kotlinx.jupyter.ext.graph.visualization.toHTML +import org.jetbrains.kotlinx.jupyter.ext.graph.wrappers.fromClass +import org.jetbrains.kotlinx.jupyter.ext.graph.wrappers.fromClassLoader import org.junit.jupiter.api.Test import java.io.File import java.io.FileOutputStream @@ -7,6 +12,7 @@ import java.io.StringWriter import java.io.Writer import javax.imageio.ImageIO import kotlin.test.assertEquals +import kotlin.test.assertTrue class RenderingTests { @Test @@ -50,6 +56,14 @@ class RenderingTests { // } } + @Test + fun testGraphVisualization() { + val html1 = Graph.of(GraphNode.fromClass()).toHTML() + assertTrue(html1.length > 1000) + val html2 = Graph.of(GraphNode.fromClassLoader()).toHTML() + assertTrue(html2.length > 1000) + } + private fun assertHtmlEquals( @Suppress("SameParameterValue") fileName: String, contentWriteAction: Writer.() -> Unit diff --git a/jupyter-lib/shared-compiler/src/main/kotlin/org/jetbrains/kotlinx/jupyter/config/defaultImports.kt b/jupyter-lib/shared-compiler/src/main/kotlin/org/jetbrains/kotlinx/jupyter/config/defaultImports.kt index ecd49be10..2aab89dac 100644 --- a/jupyter-lib/shared-compiler/src/main/kotlin/org/jetbrains/kotlinx/jupyter/config/defaultImports.kt +++ b/jupyter-lib/shared-compiler/src/main/kotlin/org/jetbrains/kotlinx/jupyter/config/defaultImports.kt @@ -2,10 +2,13 @@ package org.jetbrains.kotlinx.jupyter.config import kotlin.script.experimental.dependencies.RepositoryCoordinates -val defaultRepositories = arrayOf( - "https://repo.maven.apache.org/maven2/", - "https://jitpack.io/", -).map(::RepositoryCoordinates) +val MAVEN_CENTRAL = RepositoryCoordinates("https://repo.maven.apache.org/maven2/") +val JITPACK = RepositoryCoordinates("https://jitpack.io/") + +val defaultRepositories = listOf( + MAVEN_CENTRAL, + JITPACK, +) val defaultGlobalImports = listOf( "kotlin.math.*", diff --git a/src/test/kotlin/org/jetbrains/kotlinx/jupyter/test/ResolverTests.kt b/src/test/kotlin/org/jetbrains/kotlinx/jupyter/test/ResolverTests.kt index 500fd7570..1c10064e9 100644 --- a/src/test/kotlin/org/jetbrains/kotlinx/jupyter/test/ResolverTests.kt +++ b/src/test/kotlin/org/jetbrains/kotlinx/jupyter/test/ResolverTests.kt @@ -2,7 +2,6 @@ package org.jetbrains.kotlinx.jupyter.test import kotlinx.coroutines.runBlocking import org.jetbrains.kotlin.mainKts.impl.IvyResolver -import org.jetbrains.kotlinx.jupyter.config.defaultRepositories import org.junit.jupiter.api.Test import org.slf4j.Logger import org.slf4j.LoggerFactory @@ -15,7 +14,7 @@ class ResolverTests { private val log: Logger by lazy { LoggerFactory.getLogger("resolver") } private fun ExternalDependenciesResolver.doResolve(artifact: String): List { - defaultRepositories.forEach { addRepository(it) } + testRepositories.forEach { addRepository(it) } assertTrue(acceptsArtifact(artifact)) val result = runBlocking { resolve(artifact) } assertTrue(result is ResultWithDiagnostics.Success) diff --git a/src/test/kotlin/org/jetbrains/kotlinx/jupyter/test/repl/CustomLibraryResolverTests.kt b/src/test/kotlin/org/jetbrains/kotlinx/jupyter/test/repl/CustomLibraryResolverTests.kt index 191a19a08..76efe2adb 100644 --- a/src/test/kotlin/org/jetbrains/kotlinx/jupyter/test/repl/CustomLibraryResolverTests.kt +++ b/src/test/kotlin/org/jetbrains/kotlinx/jupyter/test/repl/CustomLibraryResolverTests.kt @@ -10,7 +10,6 @@ import org.jetbrains.kotlinx.jupyter.api.PropertyDeclaration import org.jetbrains.kotlinx.jupyter.api.declareProperties import org.jetbrains.kotlinx.jupyter.api.libraries.LibraryDefinition import org.jetbrains.kotlinx.jupyter.api.libraries.ResourceType -import org.jetbrains.kotlinx.jupyter.config.defaultRepositories import org.jetbrains.kotlinx.jupyter.defaultRuntimeProperties import org.jetbrains.kotlinx.jupyter.dependencies.ResolverConfig import org.jetbrains.kotlinx.jupyter.exceptions.LibraryProblemPart @@ -21,6 +20,7 @@ import org.jetbrains.kotlinx.jupyter.libraries.LibraryResolver import org.jetbrains.kotlinx.jupyter.libraries.Variable import org.jetbrains.kotlinx.jupyter.libraries.parseLibraryDescriptor import org.jetbrains.kotlinx.jupyter.test.library +import org.jetbrains.kotlinx.jupyter.test.testRepositories import org.jetbrains.kotlinx.jupyter.test.toLibraries import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Test @@ -41,7 +41,7 @@ class CustomLibraryResolverTests : AbstractReplTest() { classpathWithTestLib, homeDir, ResolverConfig( - defaultRepositories, + testRepositories, libs ) ) diff --git a/src/test/kotlin/org/jetbrains/kotlinx/jupyter/test/repl/IntegrationApiTests.kt b/src/test/kotlin/org/jetbrains/kotlinx/jupyter/test/repl/IntegrationApiTests.kt index b21151d16..658c4ec3c 100644 --- a/src/test/kotlin/org/jetbrains/kotlinx/jupyter/test/repl/IntegrationApiTests.kt +++ b/src/test/kotlin/org/jetbrains/kotlinx/jupyter/test/repl/IntegrationApiTests.kt @@ -5,12 +5,12 @@ import kotlinx.serialization.json.JsonPrimitive import org.jetbrains.kotlinx.jupyter.ReplForJupyterImpl import org.jetbrains.kotlinx.jupyter.api.Renderable import org.jetbrains.kotlinx.jupyter.api.libraries.LibraryDefinition -import org.jetbrains.kotlinx.jupyter.config.defaultRepositories import org.jetbrains.kotlinx.jupyter.dependencies.ResolverConfig import org.jetbrains.kotlinx.jupyter.exceptions.ReplCompilerException import org.jetbrains.kotlinx.jupyter.libraries.EmptyResolutionInfoProvider import org.jetbrains.kotlinx.jupyter.test.classpath import org.jetbrains.kotlinx.jupyter.test.library +import org.jetbrains.kotlinx.jupyter.test.testRepositories import org.jetbrains.kotlinx.jupyter.test.toLibraries import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test @@ -20,7 +20,7 @@ class IntegrationApiTests { private fun makeRepl(vararg libs: Pair): ReplForJupyterImpl { val config = ResolverConfig( - defaultRepositories, + testRepositories, libs.toList().toLibraries() ) return ReplForJupyterImpl(EmptyResolutionInfoProvider, classpath, null, config) diff --git a/src/test/kotlin/org/jetbrains/kotlinx/jupyter/test/repl/ReplWithResolverTests.kt b/src/test/kotlin/org/jetbrains/kotlinx/jupyter/test/repl/ReplWithResolverTests.kt index 65be09b6e..d7cd070be 100644 --- a/src/test/kotlin/org/jetbrains/kotlinx/jupyter/test/repl/ReplWithResolverTests.kt +++ b/src/test/kotlin/org/jetbrains/kotlinx/jupyter/test/repl/ReplWithResolverTests.kt @@ -3,7 +3,6 @@ package org.jetbrains.kotlinx.jupyter.test.repl import org.intellij.lang.annotations.Language import org.jetbrains.kotlinx.jupyter.ReplForJupyterImpl import org.jetbrains.kotlinx.jupyter.api.MimeTypedResult -import org.jetbrains.kotlinx.jupyter.config.defaultRepositories import org.jetbrains.kotlinx.jupyter.dependencies.ResolverConfig import org.jetbrains.kotlinx.jupyter.libraries.GitHubRepoName import org.jetbrains.kotlinx.jupyter.libraries.GitHubRepoOwner @@ -16,6 +15,7 @@ import org.jetbrains.kotlinx.jupyter.libraries.getStandardResolver import org.jetbrains.kotlinx.jupyter.test.TestDisplayHandler import org.jetbrains.kotlinx.jupyter.test.classpath import org.jetbrains.kotlinx.jupyter.test.standardResolverRuntimeProperties +import org.jetbrains.kotlinx.jupyter.test.testRepositories import org.jetbrains.kotlinx.jupyter.test.testResolverConfig import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Test @@ -32,7 +32,7 @@ class ReplWithResolverTests : AbstractReplTest() { private fun getReplWithStandardResolver(): ReplForJupyterImpl { val standardResolutionInfoProvider = ResolutionInfoProvider.withDefaultDirectoryResolution(homeDir.resolve(LibrariesDir)) - val config = ResolverConfig(defaultRepositories, getStandardResolver(".", standardResolutionInfoProvider)) + val config = ResolverConfig(testRepositories, getStandardResolver(".", standardResolutionInfoProvider)) return ReplForJupyterImpl(standardResolutionInfoProvider, classpath, homeDir, config, standardResolverRuntimeProperties) } diff --git a/src/test/kotlin/org/jetbrains/kotlinx/jupyter/test/testUtil.kt b/src/test/kotlin/org/jetbrains/kotlinx/jupyter/test/testUtil.kt index ae4c444ee..3a8b7fa7f 100644 --- a/src/test/kotlin/org/jetbrains/kotlinx/jupyter/test/testUtil.kt +++ b/src/test/kotlin/org/jetbrains/kotlinx/jupyter/test/testUtil.kt @@ -32,6 +32,8 @@ import kotlin.script.experimental.jvm.util.scriptCompilationClasspathFromContext const val standardResolverBranch = "master" +val testRepositories = defaultRepositories + val standardResolverRuntimeProperties = object : ReplRuntimeProperties by defaultRuntimeProperties { override val currentBranch: String get() = standardResolverBranch @@ -49,7 +51,7 @@ val classpath = scriptCompilationClasspathFromContext( val testResolverConfig: ResolverConfig get() = ResolverConfig( - defaultRepositories, + testRepositories, getResolverFromNamesMap(parseLibraryDescriptors(readLibraries())) )