Skip to content

Commit c6b1390

Browse files
nikolay-egorovileasile
authored andcommitted
Add ability of collecting and viewing variables state
- Introduce :vars command to print available variables in convenient form - Display variables in HTML table - Support private/protected/internal variables as well - Add possibility to get variables using Notebook API - Send variables using messaging protocol - Add smart updating and caching of variable values
1 parent 4d452db commit c6b1390

File tree

18 files changed

+716
-26
lines changed

18 files changed

+716
-26
lines changed

docs/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ python -m kotlin_kernel add-kernel --name "JDK 15 Big 2 GPU" --jdk ~/.jdks/openj
124124
The following REPL commands are supported:
125125
- `:help` - display help
126126
- `:classpath` - show current classpath
127+
- `:vars` - get visible variables values
127128

128129
### Dependencies resolving annotations
129130

jupyter-lib/api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/api/Notebook.kt

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
package org.jetbrains.kotlinx.jupyter.api
22

3-
import kotlin.jvm.Throws
4-
53
/**
64
* [Notebook] is a main entry point for Kotlin Jupyter API
75
*/
@@ -11,6 +9,19 @@ interface Notebook {
119
*/
1210
val cellsList: Collection<CodeCell>
1311

12+
/**
13+
* Current state of visible variables
14+
*/
15+
val variablesState: Map<String, VariableState>
16+
17+
/**
18+
* Stores info about useful variables in a cell.
19+
* Key: cellId;
20+
* Value: set of variable names.
21+
* Useful <==> declarations + modifying references
22+
*/
23+
val cellVariables: Map<Int, Set<String>>
24+
1425
/**
1526
* Mapping allowing to get cell by execution number
1627
*/
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package org.jetbrains.kotlinx.jupyter.api
2+
3+
import kotlin.reflect.KProperty
4+
import kotlin.reflect.KProperty1
5+
import kotlin.reflect.jvm.isAccessible
6+
7+
interface VariableState {
8+
val property: KProperty<*>
9+
val scriptInstance: Any?
10+
val stringValue: String?
11+
val value: Any?
12+
}
13+
14+
data class VariableStateImpl(
15+
override val property: KProperty1<Any, *>,
16+
override val scriptInstance: Any,
17+
) : VariableState {
18+
private var cachedValue: Any? = null
19+
20+
fun update() {
21+
val wasAccessible = property.isAccessible
22+
property.isAccessible = true
23+
val fieldValue = property.get(scriptInstance)
24+
property.isAccessible = wasAccessible
25+
cachedValue = fieldValue
26+
}
27+
28+
override val stringValue: String?
29+
get() = cachedValue?.toString()
30+
31+
override val value: Any?
32+
get() = cachedValue
33+
}

jupyter-lib/shared-compiler/src/main/kotlin/org/jetbrains/kotlinx/jupyter/compiler/util/serializedCompiledScript.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ class EvaluatedSnippetMetadata(
2525
val newClasspath: Classpath = emptyList(),
2626
val compiledData: SerializedCompiledScriptsData = SerializedCompiledScriptsData.EMPTY,
2727
val newImports: List<String> = emptyList(),
28+
val evaluatedVariablesState: Map<String, String?> = mutableMapOf()
2829
) {
2930
companion object {
3031
val EMPTY = EvaluatedSnippetMetadata()

kotlin-jupyter-plugin/common-dependencies/src/main/kotlin/org/jetbrains/kotlinx/jupyter/common/ReplCommand.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ package org.jetbrains.kotlinx.jupyter.common
22

33
enum class ReplCommand(val desc: String) {
44
HELP("display help"),
5-
CLASSPATH("show current classpath");
5+
CLASSPATH("show current classpath"),
6+
VARS("get visible variables values");
67

78
val nameForUser = getNameForUser(name)
89

src/main/kotlin/org/jetbrains/kotlinx/jupyter/apiImpl.kt

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ import org.jetbrains.kotlinx.jupyter.api.JREInfoProvider
99
import org.jetbrains.kotlinx.jupyter.api.KotlinKernelVersion
1010
import org.jetbrains.kotlinx.jupyter.api.Notebook
1111
import org.jetbrains.kotlinx.jupyter.api.RenderersProcessor
12-
import java.lang.IllegalStateException
12+
import org.jetbrains.kotlinx.jupyter.api.VariableState
13+
import org.jetbrains.kotlinx.jupyter.repl.InternalEvaluator
1314

1415
class DisplayResultWrapper private constructor(
1516
val display: DisplayResult,
@@ -104,6 +105,11 @@ class NotebookImpl(
104105
override val cellsList: Collection<CodeCellImpl>
105106
get() = cells.values
106107

108+
override val variablesState = mutableMapOf<String, VariableState>()
109+
110+
override val cellVariables: Map<Int, Set<String>>
111+
get() = currentCellVariables
112+
107113
override fun getCell(id: Int): CodeCellImpl {
108114
return cells[id] ?: throw ArrayIndexOutOfBoundsException(
109115
"There is no cell with number '$id'"
@@ -114,6 +120,7 @@ class NotebookImpl(
114120
return getCell(id).result
115121
}
116122

123+
private var currentCellVariables = mapOf<Int, Set<String>>()
117124
private val history = arrayListOf<CodeCellImpl>()
118125
private var mainCellCreated = false
119126

@@ -132,6 +139,32 @@ class NotebookImpl(
132139
override val jreInfo: JREInfoProvider
133140
get() = JavaRuntime
134141

142+
fun updateVariablesState(evaluator: InternalEvaluator) {
143+
variablesState += evaluator.variablesHolder
144+
currentCellVariables = evaluator.cellVariables
145+
}
146+
147+
fun updateVariablesState(varsStateUpdate: Map<String, VariableState>) {
148+
variablesState += varsStateUpdate
149+
}
150+
151+
fun variablesReportAsHTML(): String {
152+
return generateHTMLVarsReport(variablesState)
153+
}
154+
155+
fun variablesReport(): String {
156+
return if (variablesState.isEmpty()) ""
157+
else {
158+
buildString {
159+
append("Visible vars: \n")
160+
variablesState.forEach { (name, currentState) ->
161+
append("\t$name : ${currentState.stringValue}\n")
162+
}
163+
append('\n')
164+
}
165+
}
166+
}
167+
135168
fun addCell(
136169
internalId: Int,
137170
preprocessedCode: String,

src/main/kotlin/org/jetbrains/kotlinx/jupyter/commands.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.jetbrains.kotlinx.jupyter
22

3+
import org.jetbrains.kotlinx.jupyter.api.htmlResult
34
import org.jetbrains.kotlinx.jupyter.api.textResult
45
import org.jetbrains.kotlinx.jupyter.common.ReplCommand
56
import org.jetbrains.kotlinx.jupyter.common.ReplLineMagic
@@ -56,6 +57,9 @@ fun runCommand(code: String, repl: ReplForJupyter): Response {
5657
val cp = repl.currentClasspath
5758
OkResponseWithMessage(textResult("Current classpath (${cp.count()} paths):\n${cp.joinToString("\n")}"))
5859
}
60+
ReplCommand.VARS -> {
61+
OkResponseWithMessage(htmlResult(repl.notebook.variablesReportAsHTML()))
62+
}
5963
ReplCommand.HELP -> {
6064
val commands = ReplCommand.values().asIterable().joinToStringIndented { ":${it.nameForUser} - ${it.desc}" }
6165
val magics = ReplLineMagic.values().asIterable().filter { it.visibleInHelp }.joinToStringIndented {
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package org.jetbrains.kotlinx.jupyter
2+
3+
import org.jetbrains.kotlinx.jupyter.api.VariableState
4+
5+
const val varsTableStyleClass = "variables_table"
6+
7+
fun generateHTMLVarsReport(variablesState: Map<String, VariableState>): String {
8+
return buildString {
9+
append(generateStyleSection())
10+
if (variablesState.isEmpty()) {
11+
append("<h2 style=\"text-align:center;\">Variables State's Empty</h2>\n")
12+
return toString()
13+
}
14+
15+
append("<h2 style=\"text-align:center;\">Variables State</h2>\n")
16+
append(generateVarsTable(variablesState))
17+
}
18+
}
19+
20+
fun generateStyleSection(borderPx: Int = 1, paddingPx: Int = 5): String {
21+
//language=HTML
22+
val styleSection = """
23+
<style>
24+
table.$varsTableStyleClass, .$varsTableStyleClass th, .$varsTableStyleClass td {
25+
border: ${borderPx}px solid black;
26+
border-collapse: collapse;
27+
text-align:center;
28+
}
29+
.$varsTableStyleClass th, .$varsTableStyleClass td {
30+
padding: ${paddingPx}px;
31+
}
32+
</style>
33+
34+
""".trimIndent()
35+
return styleSection
36+
}
37+
38+
fun generateVarsTable(variablesState: Map<String, VariableState>): String {
39+
return buildString {
40+
append(
41+
"""
42+
<table class="$varsTableStyleClass" style="width:80%;margin-left:auto;margin-right:auto;" align="center">
43+
<tr>
44+
<th>Variable</th>
45+
<th>Value</th>
46+
</tr>
47+
48+
""".trimIndent()
49+
)
50+
51+
variablesState.entries.forEach {
52+
append(
53+
"""
54+
<tr>
55+
<td>${it.key}</td>
56+
<td>${it.value.stringValue}</td>
57+
</tr>
58+
""".trimIndent()
59+
)
60+
}
61+
62+
append("\n</table>\n")
63+
}
64+
}

src/main/kotlin/org/jetbrains/kotlinx/jupyter/repl.kt

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,9 @@ import org.jetbrains.kotlinx.jupyter.codegen.FieldsProcessor
1717
import org.jetbrains.kotlinx.jupyter.codegen.FieldsProcessorImpl
1818
import org.jetbrains.kotlinx.jupyter.codegen.FileAnnotationsProcessor
1919
import org.jetbrains.kotlinx.jupyter.codegen.FileAnnotationsProcessorImpl
20-
import org.jetbrains.kotlinx.jupyter.codegen.ResultsRenderersProcessor
2120
import org.jetbrains.kotlinx.jupyter.codegen.RenderersProcessorImpl
21+
import org.jetbrains.kotlinx.jupyter.codegen.ResultsRenderersProcessor
22+
import org.jetbrains.kotlinx.jupyter.common.ReplCommand
2223
import org.jetbrains.kotlinx.jupyter.common.looksLikeReplCommand
2324
import org.jetbrains.kotlinx.jupyter.compiler.CompilerArgsConfigurator
2425
import org.jetbrains.kotlinx.jupyter.compiler.DefaultCompilerArgsConfigurator
@@ -360,6 +361,27 @@ class ReplForJupyterImpl(
360361
else context.compilationConfiguration.asSuccess()
361362
}
362363

364+
/**
365+
* Used for debug purposes.
366+
* @see ReplCommand
367+
*/
368+
private fun printVariables(isHtmlFormat: Boolean = false) = log.debug(
369+
if (isHtmlFormat) notebook.variablesReportAsHTML() else notebook.variablesReport()
370+
)
371+
372+
private fun printUsagesInfo(cellId: Int, usedVariables: Set<String>?) {
373+
log.debug(buildString {
374+
if (usedVariables == null || usedVariables.isEmpty()) {
375+
append("No usages for cell $cellId")
376+
return@buildString
377+
}
378+
append("Usages for cell $cellId:\n")
379+
usedVariables.forEach {
380+
append(it + "\n")
381+
}
382+
})
383+
}
384+
363385
override fun eval(code: Code, displayHandler: DisplayHandler?, jupyterId: Int): EvalResult {
364386
return withEvalContext {
365387
rethrowAsLibraryException(LibraryProblemPart.BEFORE_CELL_CALLBACKS) {
@@ -371,14 +393,14 @@ class ReplForJupyterImpl(
371393
val compiledData: SerializedCompiledScriptsData
372394
val newImports: List<String>
373395
val result = try {
374-
executor.execute(code, displayHandler) { internalId, codeToExecute ->
396+
log.debug("Current cell id: $jupyterId")
397+
executor.execute(code, displayHandler, currentCellId = jupyterId - 1) { internalId, codeToExecute ->
375398
cell = notebook.addCell(internalId, codeToExecute, EvalData(jupyterId, code))
376399
}
377400
} finally {
378401
compiledData = internalEvaluator.popAddedCompiledScripts()
379402
newImports = importsCollector.popAddedImports()
380403
}
381-
382404
cell?.resultVal = result.result.value
383405

384406
val rendered = result.result.let {
@@ -395,7 +417,13 @@ class ReplForJupyterImpl(
395417
updateClasspath()
396418
} ?: emptyList()
397419

398-
EvalResult(rendered, EvaluatedSnippetMetadata(newClasspath, compiledData, newImports))
420+
notebook.updateVariablesState(internalEvaluator)
421+
// printVars()
422+
// printUsagesInfo(jupyterId, cellVariables[jupyterId - 1])
423+
424+
425+
val variablesStateUpdate = notebook.variablesState.mapValues { it.value.stringValue }
426+
EvalResult(rendered, EvaluatedSnippetMetadata(newClasspath, compiledData, newImports, variablesStateUpdate))
399427
}
400428
}
401429

src/main/kotlin/org/jetbrains/kotlinx/jupyter/repl/CellExecutor.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ interface CellExecutor : ExecutionHost {
1717
processAnnotations: Boolean = true,
1818
processMagics: Boolean = true,
1919
invokeAfterCallbacks: Boolean = true,
20+
currentCellId: Int = -1,
2021
callback: ExecutionStartedCallback? = null
2122
): InternalEvalResult
2223
}

0 commit comments

Comments
 (0)