Skip to content

Commit 1e416c7

Browse files
authoredDec 23, 2019
Library configuration improvements (#34)
* Split library descriptors into separate jsons * Load local library descriptors from "/.jupyter_kotlin/libraries" directory * Asynchronously download library descriptors from master branch of "https://github.com/Kotlin/kotlin-jupyter" repository * Cache downloaded descriptors in "/.jupyter_kotlin/cache/libraries" directory * Check library descriptor format version and suggest updating kernel if format was changed
1 parent b9dadc8 commit 1e416c7

23 files changed

+620
-328
lines changed
 

‎build.gradle

+7-7
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@ allprojects {
4444
project.getProperty('installPath') :
4545
Paths.get(System.properties['user.home'].toString(), ".ipython", "kernels", "kotlin").toAbsolutePath().toString()
4646
ext.debugPort = 1044
47-
ext.configFile = "config.json"
4847
ext.debuggerConfig = "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=$debugPort"
4948
}
5049

@@ -69,6 +68,7 @@ dependencies {
6968

7069
compile "org.apache.maven:maven-core:3.0.3"
7170
compile 'org.slf4j:slf4j-api:1.7.25'
71+
compile "khttp:khttp:1.0.0"
7272
compile 'org.zeromq:jeromq:0.3.5'
7373
compile 'com.beust:klaxon:5.2'
7474
runtime 'org.slf4j:slf4j-simple:1.7.25'
@@ -117,7 +117,7 @@ void createTaskForSpecs(Boolean debug) {
117117
} .join(File.pathSeparator)
118118
spec = substitute(spec, "RUNTIME_CLASSPATH", libsCp)
119119
spec = substitute(spec, "DEBUGGER_CONFIG", debug ? "\"$debuggerConfig\"," : "")
120-
spec = substitute(spec, "LIBRARIES_PATH", "$installPath$sep$configFile")
120+
spec = substitute(spec, "KERNEL_HOME", "$installPath")
121121
File installDir = new File("$installPath")
122122
if (!installDir.exists()) {
123123
installDir.mkdirs();
@@ -138,9 +138,9 @@ static String substitute(String spec, String template, String val) {
138138
return spec.replace("\${$template}", val.replace("\\", "\\\\"))
139139
}
140140

141-
task copyLibrariesConfig(type: Copy, dependsOn: cleanInstallDir) {
142-
from configFile
143-
into installPath
141+
task copyLibraries(type: Copy, dependsOn: cleanInstallDir) {
142+
from "libraries"
143+
into Paths.get(installPath, "libraries").toString()
144144
}
145145

146146
createTaskForSpecs(true)
@@ -151,8 +151,8 @@ task installLibs(type: Copy, dependsOn: cleanInstallDir) {
151151
from configurations.deploy
152152
}
153153

154-
task install(dependsOn: [installKernel, installLibs, createSpecs, copyLibrariesConfig]) {
154+
task install(dependsOn: [installKernel, installLibs, createSpecs, copyLibraries]) {
155155
}
156156

157-
task installDebug(dependsOn: [installKernel, installLibs, createDebugSpecs, copyLibrariesConfig]) {
157+
task installDebug(dependsOn: [installKernel, installLibs, createDebugSpecs, copyLibraries]) {
158158
}

‎config.json

-184
This file was deleted.

‎kernelspec/kernel.json.template

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"argv": [ "java", "-jar", ${DEBUGGER_CONFIG} "${KERNEL_JAR_PATH}", "{connection_file}", "-cp=${RUNTIME_CLASSPATH}", "-libs=${LIBRARIES_PATH}"],
2+
"argv": [ "java", "-jar", ${DEBUGGER_CONFIG} "${KERNEL_JAR_PATH}", "{connection_file}", "-cp=${RUNTIME_CLASSPATH}", "-home=${KERNEL_HOME}"],
33
"display_name": "Kotlin",
44
"language": "kotlin"
55
}

‎libraries/.properties

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
formatVersion=1

‎libraries/gral.json

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"properties": {
3+
"v": "0.11"
4+
},
5+
"link": "https://github.com/eseifert/gral",
6+
"dependencies": [
7+
"de.erichseifert.gral:gral-core:$v"
8+
],
9+
"imports": [
10+
"de.erichseifert.gral.data.*",
11+
"de.erichseifert.gral.data.filters.*",
12+
"de.erichseifert.gral.graphics.*",
13+
"de.erichseifert.gral.plots.*",
14+
"de.erichseifert.gral.plots.lines.*",
15+
"de.erichseifert.gral.plots.points.*",
16+
"de.erichseifert.gral.util.*"
17+
],
18+
"init": [
19+
"fun<T: Drawable> T.show(sizeX: Double, sizeY: Double): Any {\n val writer = de.erichseifert.gral.io.plots.DrawableWriterFactory.getInstance().get(\"image/svg+xml\")\n\n val buf = java.io.ByteArrayOutputStream()\n\n writer.write(this, buf, sizeX, sizeY)\n\n return MIME(writer.mimeType to buf.toString())\n}"
20+
]
21+
}

‎libraries/klaxon.json

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"properties": {
3+
"v": "5.2"
4+
},
5+
"link": "https://github.com/cbeust/klaxon",
6+
"dependencies": [
7+
"com.beust:klaxon:$v"
8+
],
9+
"imports": [
10+
"com.beust.klaxon.*"
11+
]
12+
}

‎libraries/kmath.json

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"properties": {
3+
"v": "0.1.3"
4+
},
5+
"link": "https://github.com/mipt-npm/kmath",
6+
"repositories": [
7+
"https://dl.bintray.com/mipt-npm/scientifik"
8+
],
9+
"dependencies": [
10+
"scientifik:kmath-core-jvm:$v"
11+
],
12+
"imports": [
13+
"scientifik.kmath.linear.*",
14+
"scientifik.kmath.operations.*",
15+
"scientifik.kmath.structures.*"
16+
]
17+
}

‎libraries/koma.json

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"properties": {
3+
"v": "0.13"
4+
},
5+
"link": "https://koma.kyonifer.com/index.html",
6+
"repositories": [
7+
"https://dl.bintray.com/kyonifer/maven"
8+
],
9+
"dependencies": [
10+
"com.kyonifer:koma-core-ejml:$v",
11+
"com.kyonifer:koma-plotting:$v"
12+
],
13+
"imports": [
14+
"koma.*",
15+
"koma.extensions.*"
16+
]
17+
}

‎libraries/kotlin-statistics.json

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"properties": {
3+
"v": "-SNAPSHOT"
4+
},
5+
"link": "https://github.com/thomasnield/kotlin-statistics",
6+
"dependencies": [
7+
"com.github.thomasnield:kotlin-statistics:$v"
8+
],
9+
"imports": [
10+
"org.nield.kotlinstatistics.*"
11+
]
12+
}

‎libraries/krangl.json

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"properties": {
3+
"v": "-SNAPSHOT"
4+
},
5+
"link": "https://github.com/holgerbrandl/krangl",
6+
"dependencies": [
7+
"com.github.holgerbrandl:krangl:$v"
8+
],
9+
"imports": [
10+
"krangl.*"
11+
],
12+
"init": [
13+
"fun krangl.DataFrame.toHTML(limit: Int = 20, truncate: Int = 50) : String\n{val sb = StringBuilder()\nsb.append(\"<html><body>\")\nsb.append(\"<table><tr>\")\ncols.forEach { sb.append(\"<th style=\\\"text-align:left\\\">${it.name}</th>\") }\nsb.append(\"</tr>\")\nrows.take(limit).forEach {\n sb.append(\"<tr>\")\n it.values.map{it.toString()}.forEach { \n val truncated = if (truncate > 0 && it.length > truncate) {\n if (truncate < 4) it.substring(0, truncate)\n else it.substring(0, truncate - 3) + \"...\"\n } else {\n it\n }\n sb.append(\"\"\"<td style=\"text-align:left\" title=\"$it\">$truncated</td>\"\"\") \n }\n sb.append(\"</tr>\")\n}\nsb.append(\"</table>\")\nif(limit < rows.count())\n sb.append(\"<p>... only showing top $limit rows</p>\")\nsb.append(\"</body></html>\")\nreturn sb.toString()}"
14+
],
15+
"renderers": [
16+
{
17+
"class": "krangl.SimpleDataFrame",
18+
"result": "HTML($it.toHTML())"
19+
}
20+
]
21+
}

‎libraries/kravis.json

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"properties": {
3+
"v": "-SNAPSHOT"
4+
},
5+
"link": "https://github.com/holgerbrandl/kravis",
6+
"dependencies": [
7+
"com.github.holgerbrandl:kravis:$v"
8+
],
9+
"imports": [
10+
"kravis.*"
11+
],
12+
"renderers": [
13+
{
14+
"class": "kravis.GGPlot",
15+
"result": "$it.show()"
16+
}
17+
]
18+
}

‎libraries/lets-plot.json

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"properties": {
3+
"core": "1.0.1-SNAPSHOT",
4+
"kotlin": "0.0.8-SNAPSHOT"
5+
},
6+
"link": "https://github.com/JetBrains/lets-plot-kotlin",
7+
"repositories": [
8+
"https://jetbrains.bintray.com/lets-plot-maven"
9+
],
10+
"dependencies": [
11+
"org.jetbrains.lets-plot:lets-plot-common:$core",
12+
"org.jetbrains.lets-plot:lets-plot-kotlin-api:$kotlin",
13+
"org.jetbrains.lets-plot:kotlin-frontend-api:$kotlin",
14+
"org.jetbrains.lets-plot:lets-plot-jfx:$core"
15+
],
16+
"imports": [
17+
"jetbrains.letsPlot.*",
18+
"jetbrains.letsPlot.geom.*",
19+
"jetbrains.letsPlot.stat.*"
20+
],
21+
"init": [
22+
"fun jetbrains.letsPlot.intern.Plot.getHtml() = jetbrains.letsPlot.intern.frontendContext.FrontendContextUtil.getHtml(this)",
23+
"DISPLAY(HTML(jetbrains.datalore.jupyter.configureScript()))"
24+
],
25+
"renderers": [
26+
{
27+
"class": "jetbrains.letsPlot.intern.Plot",
28+
"result": "HTML(($it as jetbrains.letsPlot.intern.Plot).getHtml())"
29+
}
30+
]
31+
}

‎libraries/spark.json

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
{
2+
"properties": {
3+
"scala": "2.11.12",
4+
"spark": "2.4.4"
5+
},
6+
"dependencies": [
7+
"org.apache.spark:spark-mllib_2.11:$spark",
8+
"org.apache.spark:spark-sql_2.11:$spark",
9+
"org.apache.spark:spark-repl_2.11:$spark",
10+
"org.apache.spark:spark-streaming-flume-assembly_2.11:$spark",
11+
"org.apache.spark:spark-graphx_2.11:$spark",
12+
"org.apache.spark:spark-launcher_2.11:$spark",
13+
"org.apache.spark:spark-catalyst_2.11:$spark",
14+
"org.apache.spark:spark-streaming_2.11:$spark",
15+
"org.apache.spark:spark-core_2.11:$spark",
16+
"org.scala-lang:scala-library:$scala",
17+
"org.scala-lang:scala-reflect:$scala",
18+
"org.scala-lang:scala-compiler:$scala",
19+
"org.scala-lang.modules:scala-xml_2.11:1.2.0",
20+
"commons-io:commons-io:2.5"
21+
],
22+
"imports": [
23+
"org.apache.spark.sql.*",
24+
"org.apache.spark.api.java.*",
25+
"org.apache.spark.ml.feature.*",
26+
"org.apache.spark.sql.functions.*"
27+
],
28+
"init": [
29+
"org.apache.log4j.Logger.getLogger(\"org\").setLevel(org.apache.log4j.Level.OFF)",
30+
"org.apache.log4j.Logger.getLogger(\"akka\").setLevel(org.apache.log4j.Level.OFF)",
31+
"val spark = SparkSession\n .builder()\n .appName(\"Spark example\")\n .master(\"local\")\n .getOrCreate()",
32+
"val sc = spark.sparkContext()",
33+
"%dumpClassesForSpark",
34+
"fun Dataset<Row>.toHTML(limit: Int = 20, truncate: Int = 50): String {\n val sb = StringBuilder()\n\n sb.append(\"<html><body>\")\n sb.append(\"\"\"<table><tr>\"\"\")\n sb.append(schema().fieldNames().map { \"<th style=\\\"text-align:left\\\">${it}</th>\"}.joinToString(\"\"))\n sb.append(\"</tr>\")\n\n limit(limit).collectAsList().forEach { row ->\n sb.append(\"<tr>\")\n (0 until row.size()).map {\n row[it].toString()\n }.forEach {\n val truncated = if (truncate > 0 && it.length > truncate) {\n if (truncate < 4) it.substring(0, truncate)\n else it.substring(0, truncate - 3) + \"...\"\n } else {\n it\n }\n sb.append(\"\"\"<td style=\"text-align:left\" title=\"$it\">$truncated</td>\"\"\")\n }\n sb.append(\"</tr>\")\n }\n sb.append(\"</table>\")\n if(limit < count())\n sb.append(\"<p>... only showing top $limit rows</p>\")\n sb.append(\"</body></html>\")\n return sb.toString()\n}"
35+
],
36+
"initCell": [
37+
"scala.Console.setOut(System.out)",
38+
"scala.Console.setErr(System.err)"
39+
],
40+
"renderers": [
41+
{
42+
"class": "org.apache.spark.sql.Dataset",
43+
"result": "HTML($it.toHTML())"
44+
}
45+
]
46+
}

‎readme.md

+26-8
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ List of supported libraries:
8888
- [koma](https://koma.kyonifer.com/index.html) - Scientific computing library
8989
- [kmath](https://github.com/mipt-npm/kmath) - Kotlin mathematical library analogous to NumPy
9090

91-
*The list of all supported libraries can be found in [config file](config.json)*
91+
*The list of all supported libraries can be found in ['libraries' directory](libraries)*
9292

9393
A definition of supported library may have a list of optional arguments that can be overriden when library is included.
9494
The major use case for library arguments is to specify particular version of library. Most library definitions default to `-SNAPSHOT` version that may be overriden in `%use` magic.
@@ -127,14 +127,14 @@ Press `TAB` to get the list of suggested items for completion.
127127
2. Run `jupyter-notebook`
128128
3. Attach remote debugger to JVM with specified port
129129

130-
## Contributing
130+
## Adding new libraries
131131

132-
### Support new libraries
132+
To support new `JVM` library and make it available via `%use` magic command you need to create a library descriptor for it.
133133

134-
You are welcome to add support for new `Kotlin` libraries by contributing to [config.json](config.json) file.
134+
Check ['libraries'](libraries) directory to see examples of library descriptors.
135135

136-
Library descriptor has the following fields:
137-
- `name`: short name of the library with optional arguments. All library arguments must have default value specified. Syntax: `<name>(<arg1>=<default1>, <arg2>=<default2>)`
136+
Library descriptor is a `<libName>.json` file with the following fields:
137+
- `properties`: a dictionary of properties that are used within library descriptor
138138
- `link`: a link to library homepage. This link will be displayed in `:help` command
139139
- `repositories`: a list of maven or ivy repositories to search for dependencies
140140
- `dependencies`: a list of library dependencies
@@ -143,8 +143,26 @@ Library descriptor has the following fields:
143143
- `initCell`: a list of code snippets to be executed before execution of any cell
144144
- `renderers`: a list of type converters for special rendering of particular types
145145

146+
*All fields are optional
147+
146148
Fields for type renderer:
147149
- `class`: fully-qualified class name for the type to be rendered
148-
- `result`: expression to produce output value. Source object is referenced as `$it`
150+
- `result`: expression that produces output value. Source object is referenced as `$it`
151+
152+
Name of the file is a library name that is passed to '%use' command
153+
154+
Library properties can be used in any parts of library descriptor as `$property`
155+
156+
To register new library descriptor:
157+
1. For private usage - add it to local settings folder `<UserHome>/.jupyter_kotlin/libraries`
158+
2. For sharing with community - commit it to ['libraries'](libraries) directory and create pull request.
159+
160+
If you are maintaining some library and want to update your library descriptor, just create pull request with your update. After your request is accepted,
161+
new version of your library will be available to all Kotlin Jupyter users immediately on next kernel startup (no kernel update is needed).
162+
163+
If a library descriptor with the same name is found in several locations, the following resolution priority is used:
164+
1. Local settings folder (highest priority)
165+
2. ['libraries'](libraries) folder at the latest master branch of `https://github.com/Kotlin/kotlin-jupyter` repository
166+
3. Kernel installation directory
149167

150-
Library arguments can be referenced in any parts of library descriptor as `$arg`
168+
If you don't want some library to be updated automatically, put fixed version of its library descriptor into local settings folder.

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ fun runCommand(code: String, repl: ReplForJupyter?): ResponseWithMessage {
3232
if (it.argumentsUsage != null) s += "\n Usage: %${it.name} ${it.argumentsUsage}"
3333
s
3434
}
35-
val libraries = repl?.config?.libraries?.toList()?.joinToStringIndented {
35+
val libraries = repl?.config?.libraries?.awaitBlocking()?.toList()?.joinToStringIndented {
3636
"${it.first} ${it.second.link ?: ""}"
3737
}
3838
ResponseWithMessage(ResponseState.Ok, textResult("Commands:\n$commands\n\nMagics\n$magics\n\nSupported libraries:\n$libraries"))

‎src/main/kotlin/org/jetbrains/kotlin/jupyter/config.kt

+232-12
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,36 @@
11
package org.jetbrains.kotlin.jupyter
22

3+
import com.beust.klaxon.JsonObject
4+
import com.beust.klaxon.Parser
5+
import khttp.responses.Response
6+
import kotlinx.coroutines.Deferred
7+
import kotlinx.coroutines.GlobalScope
8+
import kotlinx.coroutines.async
9+
import org.apache.commons.io.FileUtils
10+
import org.jetbrains.kotlin.konan.parseKonanVersion
11+
import org.json.JSONObject
312
import org.slf4j.LoggerFactory
413
import java.io.File
14+
import java.nio.file.Files
15+
import java.nio.file.Paths
516
import kotlin.script.experimental.dependencies.RepositoryCoordinates
617

18+
val LibrariesDir = "libraries"
19+
val LocalCacheDir = "cache"
20+
val CachedLibrariesFootprintFile = "libsCommit"
21+
22+
val LocalSettingsPath = Paths.get(System.getProperty("user.home"), ".jupyter_kotlin").toString()
23+
24+
val GitHubApiHost = "api.github.com"
25+
val GitHubRepoOwner = "kotlin"
26+
val GitHubRepoName = "kotlin-jupyter"
27+
val GitHubBranchName = "master"
28+
val GitHubApiPrefix = "https://$GitHubApiHost/repos/$GitHubRepoOwner/$GitHubRepoName"
29+
30+
val LibraryDescriptorExt = "json"
31+
val LibraryPropertiesFile = ".properties"
32+
val libraryDescriptorFormatVersion = 1
33+
734
internal val log by lazy { LoggerFactory.getLogger("ikotlin") }
835

936
enum class JupyterSockets {
@@ -14,9 +41,21 @@ enum class JupyterSockets {
1441
iopub
1542
}
1643

44+
data class KernelConfig(
45+
val ports: Array<Int>,
46+
val transport: String,
47+
val signatureScheme: String,
48+
val signatureKey: String,
49+
val pollingIntervalMillis: Long = 100,
50+
val scriptClasspath: List<File> = emptyList(),
51+
val resolverConfig: ResolverConfig?
52+
)
53+
54+
val protocolVersion = "5.3"
55+
1756
data class TypeRenderer(val className: String, val displayCode: String?, val resultCode: String?)
1857

19-
data class Variable(val name: String?, val value: String?)
58+
data class Variable(val name: String, val value: String)
2059

2160
class LibraryDefinition(val dependencies: List<String>,
2261
val variables: List<Variable>,
@@ -27,16 +66,197 @@ class LibraryDefinition(val dependencies: List<String>,
2766
val renderers: List<TypeRenderer>,
2867
val link: String?)
2968

30-
data class ResolverConfig(val repositories: List<RepositoryCoordinates>, val libraries: Map<String, LibraryDefinition>)
69+
data class ResolverConfig(val repositories: List<RepositoryCoordinates>,
70+
val libraries: Deferred<Map<String, LibraryDefinition>>)
3171

32-
data class KernelConfig(
33-
val ports: Array<Int>,
34-
val transport: String,
35-
val signatureScheme: String,
36-
val signatureKey: String,
37-
val pollingIntervalMillis: Long = 100,
38-
val scriptClasspath: List<File> = emptyList(),
39-
val resolverConfig: ResolverConfig?
40-
)
72+
fun parseLibraryArgument(str: String): Variable {
73+
val eq = str.indexOf('=')
74+
return if (eq == -1) Variable("", str.trim())
75+
else Variable(str.substring(0, eq).trim(), str.substring(eq + 1).trim())
76+
}
77+
78+
fun parseLibraryName(str: String): Pair<String, List<Variable>> {
79+
val brackets = str.indexOf('(')
80+
if (brackets == -1) return str.trim() to emptyList()
81+
val name = str.substring(0, brackets).trim()
82+
val args = str.substring(brackets + 1, str.indexOf(')', brackets))
83+
.split(',')
84+
.map(::parseLibraryArgument)
85+
return name to args
86+
}
87+
88+
fun readLibraries(basePath: String? = null, filter: (File) -> Boolean = { true }): List<Pair<String, JsonObject>> {
89+
val parser = Parser.default()
90+
return File(basePath, LibrariesDir)
91+
.listFiles()?.filter { it.extension == LibraryDescriptorExt && filter(it) }
92+
?.map {
93+
log.info("Loading '${it.nameWithoutExtension}' descriptor from '${it.canonicalPath}'")
94+
it.nameWithoutExtension to parser.parse(it.canonicalPath) as JsonObject
95+
}
96+
.orEmpty()
97+
}
98+
99+
fun getLatestCommitToLibraries(sinceTimestamp: String?): Pair<String, String>? =
100+
log.catchAll {
101+
var url = "$GitHubApiPrefix/commits?path=$LibrariesDir&sha=$GitHubBranchName"
102+
if (sinceTimestamp != null)
103+
url += "&since=$sinceTimestamp"
104+
log.info("Checking for new commits to library descriptors at $url")
105+
val arr = getHttp(url).jsonArray
106+
if (arr.length() == 0) {
107+
if (sinceTimestamp != null)
108+
getLatestCommitToLibraries(null)
109+
else {
110+
log.info("Didn't find any commits to '$LibrariesDir' at $url")
111+
null
112+
}
113+
} else {
114+
val commit = arr[0] as JSONObject
115+
val sha = commit["sha"] as String
116+
val timestamp = ((commit["commit"] as JSONObject)["committer"] as JSONObject)["date"] as String
117+
sha to timestamp
118+
}
119+
}
120+
121+
fun getHttp(url: String): Response {
122+
val response = khttp.get(url)
123+
if (response.statusCode != 200)
124+
throw Exception("Http request failed. Url = $url. Response = $response")
125+
return response
126+
}
127+
128+
fun getLibraryDescriptorVersion(commitSha: String) =
129+
log.catchAll {
130+
val url = "$GitHubApiPrefix/contents/$LibrariesDir/$LibraryPropertiesFile?ref=$commitSha"
131+
log.info("Checking current library descriptor format version from $url")
132+
val response = getHttp(url)
133+
val downloadUrl = response.jsonObject["download_url"].toString()
134+
val downloadResult = getHttp(downloadUrl)
135+
val result = downloadResult.text.parseIniConfig()["formatVersion"]!!.toInt()
136+
log.info("Current library descriptor format version: $result")
137+
result
138+
}
139+
140+
/***
141+
* Downloads library descriptors from GitHub to local cache if new commits in `libraries` directory were detected
142+
*/
143+
fun downloadNewLibraryDescriptors() {
144+
145+
// Read commit hash and timestamp for locally cached libraries.
146+
// Timestamp is used as parameter for commits request to reduce output
147+
148+
val footprintFilePath = Paths.get(LocalSettingsPath, LocalCacheDir, CachedLibrariesFootprintFile).toString()
149+
log.info("Reading commit info for which library descriptors were cached: '$footprintFilePath'")
150+
val footprintFile = File(footprintFilePath)
151+
val footprint = footprintFile.tryReadIniConfig()
152+
val timestampRegex = """\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z""".toRegex()
153+
val syncedCommitTimestamp = footprint?.get("timestamp")?.validOrNull { timestampRegex.matches(it) }
154+
val syncedCommitSha = footprint?.get("sha")
155+
log.info("Local libraries are cached for commit '$syncedCommitSha' at '$syncedCommitTimestamp'")
156+
157+
val (latestCommitSha, latestCommitTimestamp) = getLatestCommitToLibraries(syncedCommitTimestamp) ?: return
158+
if (latestCommitSha.equals(syncedCommitSha)) {
159+
log.info("No new commits to library descriptors were detected")
160+
return
161+
}
162+
163+
// Download library descriptor version
164+
165+
val descriptorVersion = getLibraryDescriptorVersion(latestCommitSha) ?: return
166+
if (descriptorVersion != libraryDescriptorFormatVersion) {
167+
if (descriptorVersion < libraryDescriptorFormatVersion)
168+
log.error("Incorrect library descriptor version in GitHub repository: $descriptorVersion")
169+
else
170+
log.warn("Kotlin Kernel needs to be updated to the latest version. Couldn't download new library descriptors from GitHub repository because their format was changed")
171+
return
172+
}
173+
174+
// Download library descriptors
175+
176+
log.info("New commits to library descriptors were detected. Downloading library descriptors for commit $latestCommitSha")
177+
178+
val libraries = log.catchAll {
179+
val url = "$GitHubApiPrefix/contents/$LibrariesDir?ref=$latestCommitSha"
180+
log.info("Requesting the list of library descriptors at $url")
181+
val response = getHttp(url)
182+
val filenameRegex = """[\w.-]+\.$LibraryDescriptorExt""".toRegex()
183+
184+
response.jsonArray.mapNotNull {
185+
val o = it as JSONObject
186+
val filename = o["name"] as String
187+
if (filenameRegex.matches(filename)) {
188+
val libUrl = o["download_url"].toString()
189+
log.info("Downloading '$filename' from $libUrl")
190+
val res = getHttp(libUrl)
191+
val text = res.jsonObject.toString()
192+
filename to text
193+
} else null
194+
}
195+
} ?: return
196+
197+
// Save library descriptors to local cache
198+
199+
val librariesPath = Paths.get(LocalSettingsPath, LocalCacheDir, LibrariesDir)
200+
val librariesDir = librariesPath.toFile()
201+
log.info("Saving ${libraries.count()} library descriptors to local cache at '$librariesPath'")
202+
try {
203+
FileUtils.deleteDirectory(librariesDir)
204+
Files.createDirectories(librariesPath)
205+
libraries.forEach {
206+
File(librariesDir.toString(), it.first).writeText(it.second)
207+
}
208+
footprintFile.writeText("""
209+
timestamp=$latestCommitTimestamp
210+
sha=$latestCommitSha
211+
""".trimIndent())
212+
} catch (e: Exception) {
213+
log.error("Failed to write downloaded library descriptors to local cache:", e)
214+
log.catchAll { FileUtils.deleteDirectory(librariesDir) }
215+
}
216+
}
217+
218+
fun getLibrariesJsons(homeDir: String): Map<String, JsonObject> {
219+
220+
downloadNewLibraryDescriptors()
221+
222+
val pathsToCheck = arrayOf(LocalSettingsPath,
223+
Paths.get(LocalSettingsPath, LocalCacheDir).toString(),
224+
homeDir)
225+
226+
val librariesMap = mutableMapOf<String, JsonObject>()
227+
228+
pathsToCheck.forEach {
229+
readLibraries(it) { !librariesMap.containsKey(it.nameWithoutExtension) }
230+
.forEach { librariesMap.put(it.first, it.second) }
231+
}
232+
233+
return librariesMap
234+
}
235+
236+
fun loadResolverConfig(homeDir: String) = ResolverConfig(defaultRepositories, GlobalScope.async {
237+
parserLibraryDescriptors(getLibrariesJsons(homeDir))
238+
})
239+
240+
val defaultRepositories = arrayOf(
241+
"https://jcenter.bintray.com/",
242+
"https://repo.maven.apache.org/maven2/",
243+
"https://jitpack.io"
244+
).map { RepositoryCoordinates(it) }
245+
246+
fun parserLibraryDescriptors(libJsons: Map<String, JsonObject>): Map<String, LibraryDefinition> {
247+
return libJsons.mapValues {
248+
LibraryDefinition(
249+
dependencies = it.value.array<String>("dependencies")?.toList().orEmpty(),
250+
variables = it.value.obj("properties")?.map { Variable(it.key, it.value.toString()) }.orEmpty(),
251+
imports = it.value.array<String>("imports")?.toList().orEmpty(),
252+
repositories = it.value.array<String>("repositories")?.toList().orEmpty(),
253+
init = it.value.array<String>("init")?.toList().orEmpty(),
254+
initCell = it.value.array<String>("initCell")?.toList().orEmpty(),
255+
renderers = it.value.array<JsonObject>("renderers")?.map {
256+
TypeRenderer(it.string("class")!!, it.string("display"), it.string("result"))
257+
}?.toList().orEmpty(),
258+
link = it.value.string("link")
259+
)
260+
}
261+
}
41262

42-
val protocolVersion = "5.3"

‎src/main/kotlin/org/jetbrains/kotlin/jupyter/ikotlin.kt

+9-49
Original file line numberDiff line numberDiff line change
@@ -5,27 +5,24 @@ import com.beust.klaxon.Parser
55
import java.io.File
66
import java.util.concurrent.atomic.AtomicLong
77
import kotlin.concurrent.thread
8-
import kotlin.script.experimental.dependencies.RepositoryCoordinates
98
import kotlin.script.experimental.jvm.util.classpathFromClassloader
109

11-
val DefaultConfigFile = "config.json"
12-
1310
data class KernelArgs(val cfgFile: File,
1411
val scriptClasspath: List<File>,
15-
val libs: File?)
12+
val homeDir: File?)
1613

1714
private fun parseCommandLine(vararg args: String): KernelArgs {
1815
var cfgFile: File? = null
1916
var classpath: List<File>? = null
20-
var libsJson: File? = null
17+
var homeDir: File? = null
2118
args.forEach {
2219
when {
2320
it.startsWith("-cp=") || it.startsWith("-classpath=") -> {
2421
if (classpath != null) throw IllegalArgumentException("classpath already set to ${classpath!!.joinToString(File.pathSeparator)}")
2522
classpath = it.substringAfter('=').split(File.pathSeparator).map { File(it) }
2623
}
27-
it.startsWith("-libs=") -> {
28-
libsJson = File(it.substringAfter('='))
24+
it.startsWith("-home=") -> {
25+
homeDir = File(it.substringAfter('='))
2926
}
3027
else -> {
3128
if (cfgFile != null) throw IllegalArgumentException("config file already set to $cfgFile")
@@ -35,7 +32,7 @@ private fun parseCommandLine(vararg args: String): KernelArgs {
3532
}
3633
if (cfgFile == null) throw IllegalArgumentException("config file is not provided")
3734
if (!cfgFile!!.exists() || !cfgFile!!.isFile) throw IllegalArgumentException("invalid config file $cfgFile")
38-
return KernelArgs(cfgFile!!, classpath ?: emptyList(), libsJson)
35+
return KernelArgs(cfgFile!!, classpath ?: emptyList(), homeDir)
3936
}
4037

4138
fun printClassPath() {
@@ -48,49 +45,12 @@ fun printClassPath() {
4845
log.info("Current classpath: " + cp.joinToString())
4946
}
5047

51-
fun parseLibraryName(str: String): Pair<String, List<Variable>> {
52-
val pattern = """\w+(\w+)?""".toRegex().matches(str)
53-
val brackets = str.indexOf('(')
54-
if (brackets == -1) return str.trim() to emptyList()
55-
val name = str.substring(0, brackets).trim()
56-
val args = str.substring(brackets + 1, str.indexOf(')', brackets))
57-
.split(',')
58-
.map {
59-
val eq = it.indexOf('=')
60-
if (eq == -1) Variable(it.trim(), null)
61-
else Variable(it.substring(0, eq).trim(), it.substring(eq + 1).trim())
62-
}
63-
return name to args
64-
}
65-
66-
fun readResolverConfig(file: File = File(DefaultConfigFile)): ResolverConfig =
67-
parseResolverConfig(Parser().parse(file.canonicalPath) as JsonObject)
68-
69-
fun parseResolverConfig(json: JsonObject): ResolverConfig {
70-
val repos = json.array<String>("repositories")?.map { RepositoryCoordinates(it) }.orEmpty()
71-
val artifacts = json.array<JsonObject>("libraries")?.map {
72-
val (name, variables) = parseLibraryName(it.string("name")!!)
73-
name to LibraryDefinition(
74-
dependencies = it.array<String>("dependencies")?.toList().orEmpty(),
75-
variables = variables,
76-
imports = it.array<String>("imports")?.toList().orEmpty(),
77-
repositories = it.array<String>("repositories")?.toList().orEmpty(),
78-
init = it.array<String>("init")?.toList().orEmpty(),
79-
initCell = it.array<String>("initCell")?.toList().orEmpty(),
80-
renderers = it.array<JsonObject>("renderers")?.map {
81-
TypeRenderer(it.string("class")!!, it.string("display"), it.string("result"))
82-
}?.toList().orEmpty(),
83-
link = it.string("link")
84-
)
85-
}?.toMap()
86-
return ResolverConfig(repos, artifacts.orEmpty())
87-
}
88-
8948
fun main(vararg args: String) {
9049
try {
9150
log.info("Kernel args: "+ args.joinToString { it })
92-
val (cfgFile, scriptClasspath, librariesConfigFile) = parseCommandLine(*args)
93-
val cfgJson = Parser().parse(cfgFile.canonicalPath) as JsonObject
51+
val (cfgFile, scriptClasspath, homeDir) = parseCommandLine(*args)
52+
val rootPath = homeDir!!.toString()
53+
val cfgJson = Parser.default().parse(cfgFile.canonicalPath) as JsonObject
9454
fun JsonObject.getInt(field: String): Int = int(field) ?: throw RuntimeException("Cannot find $field in $cfgFile")
9555

9656
val sigScheme = cfgJson.string("signature_scheme")
@@ -102,7 +62,7 @@ fun main(vararg args: String) {
10262
signatureScheme = sigScheme ?: "hmac1-sha256",
10363
signatureKey = if (sigScheme == null || key == null) "" else key,
10464
scriptClasspath = scriptClasspath,
105-
resolverConfig = librariesConfigFile?.let { readResolverConfig(it) }
65+
resolverConfig = loadResolverConfig(rootPath)
10666
))
10767
} catch (e: Exception) {
10868
log.error("exception running kernel with args: \"${args.joinToString()}\"", e)

‎src/main/kotlin/org/jetbrains/kotlin/jupyter/libraries.kt

+13-17
Original file line numberDiff line numberDiff line change
@@ -20,24 +20,21 @@ class LibrariesProcessor {
2020
* @return A name-to-value map of library arguments
2121
*/
2222
private fun substituteArguments(parameters: List<Variable>, arguments: List<Variable>): Map<String, String> {
23-
val firstNamed = arguments.indexOfFirst { it.name != null }
24-
if (firstNamed != -1 && arguments.asSequence().drop(firstNamed).any { it.name == null })
25-
throw ReplCompilerException("Mixing named and positional arguments is not allowed")
26-
val parameterNames = parameters.map { it.name!! }.toSet()
2723
val result = mutableMapOf<String, String>()
28-
for (i in 0 until arguments.count()) {
29-
if (i >= parameters.count())
24+
if (arguments.any { it.name.isEmpty() }) {
25+
if (parameters.count() != 1)
26+
throw ReplCompilerException("Unnamed argument is allowed only if library has a single property")
27+
if (arguments.count() != 1)
3028
throw ReplCompilerException("Too many arguments")
31-
val name = arguments[i].name?.also {
32-
if (!parameterNames.contains(it)) throw ReplCompilerException("Can not find parameter with name '$it'")
33-
} ?: parameters[i].name!!
29+
result[parameters[0].name] = arguments[0].value
30+
return result
31+
}
3432

35-
if (result.containsKey(name)) throw ReplCompilerException("An argument for parameter '$name' is already passed")
36-
result[name] = arguments[i].value!!
33+
arguments.forEach {
34+
result[it.name] = it.value
3735
}
3836
parameters.forEach {
39-
if (!result.containsKey(it.name!!)) {
40-
if (it.value == null) throw ReplCompilerException("No value passed for parameter '${it.name}'")
37+
if (!result.containsKey(it.name)) {
4138
result[it.name] = it.value
4239
}
4340
}
@@ -95,11 +92,10 @@ class LibrariesProcessor {
9592

9693
splitLibraryCalls(arg).forEach {
9794
val (name, vars) = parseLibraryName(it)
98-
val library = repl.config?.libraries?.get(name) ?: throw ReplCompilerException("Unknown library '$name'")
95+
val library = repl.config?.libraries?.awaitBlocking()?.get(name)
96+
?: throw ReplCompilerException("Unknown library '$name'")
9997

100-
// treat single strings in parsed arguments as values, not names
101-
val arguments = vars.map { if (it.value == null) Variable(null, it.name) else it }
102-
val mapping = substituteArguments(library.variables, arguments)
98+
val mapping = substituteArguments(library.variables, vars)
10399

104100
processedLibraries.add(LibraryWithCode(library, generateCode(repl, library, mapping)))
105101
}

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

+12-7
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,11 @@ class ReplForJupyter(val scriptClasspath: List<File> = emptyList(),
4242

4343
private val resolver = JupyterScriptDependenciesResolver(config)
4444

45-
private val renderers = config?.let { it.libraries.flatMap { it.value.renderers } }?.map { it.className to it }?.toMap().orEmpty()
45+
private val renderers = config?.let {
46+
it.libraries.asyncLet {
47+
it.flatMap { it.value.renderers }.map { it.className to it }.toMap()
48+
}
49+
}
4650

4751
private val includedLibraries = mutableSetOf<LibraryDefinition>()
4852

@@ -88,9 +92,8 @@ class ReplForJupyter(val scriptClasspath: List<File> = emptyList(),
8892
val kt = KotlinType(receiver.javaClass.canonicalName)
8993
implicitReceivers.invoke(listOf(kt))
9094

91-
val classes = listOf(/*receiver.javaClass,*/ ScriptTemplateWithDisplayHelpers::class.java)
92-
val classPath = classes.asSequence().map { it.protectionDomain.codeSource.location.path }.joinToString(":")
93-
compilerOptions.invoke(listOf("-classpath", classPath, "-jvm-target", "1.8"))
95+
log.info("Classpath for compiler options: none")
96+
compilerOptions.invoke(listOf("-jvm-target", "1.8"))
9497
}
9598
}
9699

@@ -123,6 +126,7 @@ class ReplForJupyter(val scriptClasspath: List<File> = emptyList(),
123126
val scriptClassloader = URLClassLoader(scriptClasspath.map { it.toURI().toURL() }.toTypedArray(), filteringClassLoader)
124127
baseClassLoader(scriptClassloader)
125128
}
129+
constructorArgs()
126130
}
127131

128132
private var executionCounter = 0
@@ -167,7 +171,7 @@ class ReplForJupyter(val scriptClasspath: List<File> = emptyList(),
167171

168172
init {
169173
// TODO: to be removed after investigation of https://github.com/kotlin/kotlin-jupyter/issues/24
170-
eval("1")
174+
doEval("1")
171175
}
172176

173177
fun eval(code: String, jupyterId: Int = -1): EvalResult {
@@ -226,8 +230,9 @@ class ReplForJupyter(val scriptClasspath: List<File> = emptyList(),
226230
}
227231
}
228232

229-
if (result != null) {
230-
renderers[result.javaClass.canonicalName]?.let {
233+
if (result != null && renderers != null) {
234+
val resultType = result.javaClass.canonicalName
235+
renderers.awaitBlocking()[resultType]?.let {
231236
it.displayCode?.let {
232237
doEval(it.replace("\$it", "res$replId")).value?.let(displays::add)
233238
}

‎src/main/kotlin/org/jetbrains/kotlin/jupyter/resolver.kt

+30-8
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,22 @@ import jupyter.kotlin.DependsOn
44
import jupyter.kotlin.Repository
55
import kotlinx.coroutines.runBlocking
66
import org.jetbrains.kotlin.mainKts.impl.IvyResolver
7+
import org.slf4j.LoggerFactory
78
import java.io.File
89
import kotlin.script.dependencies.ScriptContents
9-
import kotlin.script.experimental.api.*
10-
import kotlin.script.experimental.dependencies.*
10+
import kotlin.script.experimental.api.ResultWithDiagnostics
11+
import kotlin.script.experimental.api.ScriptDiagnostic
12+
import kotlin.script.experimental.api.asSuccess
13+
import kotlin.script.experimental.api.makeFailureResult
14+
import kotlin.script.experimental.dependencies.CompoundDependenciesResolver
15+
import kotlin.script.experimental.dependencies.ExternalDependenciesResolver
16+
import kotlin.script.experimental.dependencies.FileSystemDependenciesResolver
17+
import kotlin.script.experimental.dependencies.tryAddRepository
1118

1219
open class JupyterScriptDependenciesResolver(resolverConfig: ResolverConfig?) {
1320

21+
private val log by lazy { LoggerFactory.getLogger("resolver") }
22+
1423
private val resolver: ExternalDependenciesResolver
1524

1625
init {
@@ -33,17 +42,30 @@ open class JupyterScriptDependenciesResolver(resolverConfig: ResolverConfig?) {
3342
script.annotations.forEach { annotation ->
3443
when (annotation) {
3544
is Repository -> {
45+
log.info("Adding repository: ${annotation.value}")
3646
if (!resolver.tryAddRepository(annotation.value))
3747
throw IllegalArgumentException("Illegal argument for Repository annotation: $annotation")
3848
}
3949
is DependsOn -> {
40-
val result = runBlocking { resolver.resolve(annotation.value) }
41-
when (result) {
42-
is ResultWithDiagnostics.Failure -> scriptDiagnostics.add(ScriptDiagnostic("Failed to resolve dependencies:\n" + result.reports.joinToString("\n") { it.message }))
43-
is ResultWithDiagnostics.Success -> {
44-
addedClasspath.addAll(result.value)
45-
classpath.addAll(result.value)
50+
log.info("Resolving ${annotation.value}")
51+
try {
52+
val result = runBlocking { resolver.resolve(annotation.value) }
53+
when (result) {
54+
is ResultWithDiagnostics.Failure -> {
55+
val diagnostics = ScriptDiagnostic("Failed to resolve ${annotation.value}:\n" + result.reports.joinToString("\n") { it.message })
56+
log.warn(diagnostics.message, diagnostics.exception)
57+
scriptDiagnostics.add(diagnostics)
58+
}
59+
is ResultWithDiagnostics.Success -> {
60+
log.info("Resolved: " + result.value.joinToString())
61+
addedClasspath.addAll(result.value)
62+
classpath.addAll(result.value)
63+
}
4664
}
65+
} catch (e: Exception) {
66+
val diagnostic = ScriptDiagnostic("Unhandled exception during resolve", exception = e)
67+
log.error(diagnostic.message, e)
68+
scriptDiagnostics.add(diagnostic)
4769
}
4870
}
4971
else -> throw Exception("Unknown annotation ${annotation.javaClass}")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package org.jetbrains.kotlin.jupyter
2+
3+
import com.beust.klaxon.JsonObject
4+
import com.beust.klaxon.Parser
5+
import kotlinx.coroutines.Deferred
6+
import kotlinx.coroutines.GlobalScope
7+
import kotlinx.coroutines.async
8+
import kotlinx.coroutines.runBlocking
9+
import org.json.JSONObject
10+
import org.slf4j.Logger
11+
import java.io.File
12+
import java.io.StringReader
13+
import javax.xml.bind.JAXBElement
14+
15+
fun <T> catchAll(body: () -> T): T? = try {
16+
body()
17+
} catch (e: Exception) {
18+
null
19+
}
20+
21+
fun <T> Logger.catchAll(msg: String = "", body: () -> T): T? = try {
22+
body()
23+
} catch (e: Exception) {
24+
this.error(msg, e)
25+
null
26+
}
27+
28+
fun <T> T.validOrNull(predicate: (T) -> Boolean): T? = if (predicate(this)) this else null
29+
30+
fun <T> T.asDeferred(): Deferred<T> = this.let { GlobalScope.async { it } }
31+
32+
fun File.existsOrNull() = if (exists()) this else null
33+
34+
fun <T, R> Deferred<T>.asyncLet(selector: suspend (T) -> R): Deferred<R> = this.let {
35+
GlobalScope.async {
36+
selector(it.await())
37+
}
38+
}
39+
40+
fun <T> Deferred<T>.awaitBlocking(): T = if (isCompleted) getCompleted() else runBlocking { await() }
41+
42+
fun String.parseIniConfig() =
43+
split("\n").map { it.split('=') }.filter { it.count() == 2 }.map { it[0] to it[1] }.toMap()
44+
45+
fun File.tryReadIniConfig() =
46+
existsOrNull()?.let {
47+
catchAll { it.readText().parseIniConfig() }
48+
}
49+
50+
fun readJson(path: String) =
51+
Parser.default().parse(path) as JsonObject
52+
53+
fun JSONObject.toJsonObject() = Parser.default().parse(StringReader(toString())) as JsonObject

‎src/test/kotlin/org/jetbrains/kotlin/jupyter/test/parseArgumentsTests.kt

+4-4
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,18 @@ class ParseArgumentsTests {
1818
val (name, args) = parseLibraryName("lib(arg1)")
1919
Assert.assertEquals("lib", name)
2020
Assert.assertEquals(1, args.count())
21-
Assert.assertEquals("arg1", args[0].name)
22-
Assert.assertNull(args[0].value)
21+
Assert.assertEquals("arg1", args[0].value)
22+
Assert.assertEquals("", args[0].name)
2323
}
2424

2525
@Test
2626
fun test3() {
27-
val (name, args) = parseLibraryName("lib (arg1 = 1.2, arg2)")
27+
val (name, args) = parseLibraryName("lib (arg1 = 1.2, arg2 = val2)")
2828
Assert.assertEquals("lib", name)
2929
Assert.assertEquals(2, args.count())
3030
Assert.assertEquals("arg1", args[0].name)
3131
Assert.assertEquals("1.2", args[0].value)
3232
Assert.assertEquals("arg2", args[1].name)
33-
Assert.assertNull(args[1].value)
33+
Assert.assertEquals("val2", args[1].value)
3434
}
3535
}

‎src/test/kotlin/org/jetbrains/kotlin/jupyter/test/replTests.kt

+36-30
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,10 @@ package org.jetbrains.kotlin.jupyter.test
22

33
import com.beust.klaxon.JsonObject
44
import com.beust.klaxon.Parser
5-
import jupyter.kotlin.DisplayResult
65
import jupyter.kotlin.MimeTypedResult
7-
import org.jetbrains.kotlin.jupyter.ReplForJupyter
8-
import org.jetbrains.kotlin.jupyter.parseResolverConfig
9-
import org.jetbrains.kotlin.jupyter.readResolverConfig
6+
import kotlinx.coroutines.GlobalScope
7+
import kotlinx.coroutines.async
8+
import org.jetbrains.kotlin.jupyter.*
109
import org.jetbrains.kotlin.jupyter.repl.completion.CompletionResultSuccess
1110
import org.junit.Assert
1211
import org.junit.Test
@@ -16,6 +15,9 @@ import kotlin.test.assertNotNull
1615

1716
class ReplTest {
1817

18+
fun replWithResolver() = ReplForJupyter(classpath, ResolverConfig(defaultRepositories,
19+
parserLibraryDescriptors(readLibraries().toMap()).asDeferred()))
20+
1921
@Test
2022
fun TestRepl() {
2123
val repl = ReplForJupyter(classpath)
@@ -73,14 +75,14 @@ class ReplTest {
7375

7476
@Test
7577
fun TestUseMagic() {
76-
val config = """
77-
{
78-
"libraries": [
78+
val lib1 = "mylib" to """
7979
{
80-
"name": "mylib(v1, v2=2.3)",
80+
"properties": {
81+
"v1": "0.2"
82+
},
8183
"dependencies": [
82-
"artifact1:""" + "\$v1" + """",
83-
"artifact2:""" + "\$v2" + """"
84+
"artifact1:${'$'}v1",
85+
"artifact2:${'$'}v1"
8486
],
8587
"imports": [
8688
"package1",
@@ -90,31 +92,35 @@ class ReplTest {
9092
"code1",
9193
"code2"
9294
]
93-
},
94-
{
95-
"name": "other(a=temp, b=test)",
96-
"dependencies": [
97-
"path-""" + "\$a" + """",
98-
"path-""" + "\$b" + """"
99-
],
100-
"imports": [
101-
"otherPackage"
102-
]
103-
}
104-
]
105-
}
95+
}""".trimIndent()
96+
val lib2 = "other" to """
97+
{
98+
"properties": {
99+
"a": "temp",
100+
"b": "test"
101+
},
102+
"dependencies": [
103+
"path-${'$'}a",
104+
"path-${'$'}b"
105+
],
106+
"imports": [
107+
"otherPackage"
108+
]
109+
}
106110
""".trimIndent()
107-
val json = Parser().parse(StringBuilder(config)) as JsonObject
108-
val replConfig = parseResolverConfig(json)
109-
val repl = ReplForJupyter(classpath, replConfig)
111+
val parser = Parser.default()
112+
113+
val libJsons = arrayOf(lib1, lib2).map { it.first to parser.parse(StringBuilder(it.second)) as JsonObject }.toMap()
114+
115+
val repl = ReplForJupyter(classpath, ResolverConfig(defaultRepositories, parserLibraryDescriptors(libJsons).asDeferred()))
110116
val res = repl.preprocessCode("%use mylib(1.0), other(b=release, a=debug)").trimIndent()
111117
val libs = repl.librariesCodeGenerator.getProcessedLibraries()
112118
assertEquals("", res)
113119
assertEquals(2, libs.count())
114120
arrayOf(
115121
"""
116122
@file:DependsOn("artifact1:1.0")
117-
@file:DependsOn("artifact2:2.3")
123+
@file:DependsOn("artifact2:1.0")
118124
import package1
119125
import package2
120126
code1
@@ -132,7 +138,7 @@ class ReplTest {
132138

133139
@Test
134140
fun TestLetsPlot() {
135-
val repl = ReplForJupyter(classpath, readResolverConfig())
141+
val repl = replWithResolver()
136142
val code1 = "%use lets-plot"
137143
val code2 = """lets_plot(mapOf<String, Any>("cat" to listOf("a", "b")))"""
138144
val res1 = repl.eval(code1)
@@ -149,7 +155,7 @@ class ReplTest {
149155

150156
@Test
151157
fun TestTwoLibrariesInUse() {
152-
val repl = ReplForJupyter(classpath, readResolverConfig())
158+
val repl = replWithResolver()
153159
val code = "%use lets-plot, krangl"
154160
val res = repl.eval(code)
155161
assertEquals(1, res.displayValues.count())
@@ -158,7 +164,7 @@ class ReplTest {
158164
@Test
159165
//TODO: https://github.com/Kotlin/kotlin-jupyter/issues/25
160166
fun TestKranglImportInfixFun() {
161-
val repl = ReplForJupyter(classpath, readResolverConfig())
167+
val repl = replWithResolver()
162168
val code = """%use krangl
163169
"a" to {it["a"]}"""
164170
val res = repl.eval(code)

0 commit comments

Comments
 (0)
Please sign in to comment.