Skip to content

Commit b4d6610

Browse files
committed
Add type names filtering for integration classes
Fixes #363
1 parent f5df30d commit b4d6610

File tree

17 files changed

+352
-17
lines changed

17 files changed

+352
-17
lines changed

docs/libraries.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ Library descriptor is a `<libName>.json` file with the following fields:
2424
- `renderers`: a mapping from fully qualified names of types to be rendered to the Kotlin expression returning output value.
2525
Source object is referenced as `$it`
2626
- `resources`: a list of JS/CSS resources. See [this descriptor](../src/test/testData/lib-with-resources.json) for example
27+
- `integrationTypeNameRules`: a list of rules for integration classes which are about to be loaded transitively. Each rule has the form `[+|-]:<pattern>` where `+` or `-` denotes if this pattern is accepted or declined. Pattern may consist of any characters. Special combinations are allowed: `?` (any single character or no character), `*` (any character excluding dot), `**` (any character).
2728

2829
*All fields are optional
2930

@@ -71,7 +72,7 @@ plugins {
7172
This plugin adds following dependencies to your project:
7273

7374
| Artifact | Gradle option to exclude/include | Enabled by default | Dependency scope | Method for adding dependency manually |
74-
| :------------------------------- | :------------------------------- | :----------------- | :------------------- | :--------------------------------------- |
75+
|:---------------------------------|:---------------------------------|:-------------------|:---------------------|:-----------------------------------------|
7576
| `kotlin-jupyter-api` | `kotlin.jupyter.add.api` | yes | `compileOnly` | `addApiDependency(version: String?)` |
7677
| `kotlin-jupyter-api-annotations` | `kotlin.jupyter.add.scanner` | no | `compileOnly` | `addScannerDependency(version: String?)` |
7778
| `kotlin-jupyter-test-kit` | `kotlin.jupyter.add.testkit` | yes | `testImplementation` | `addTestKitDependency(version: String?)` |

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@ interface KotlinKernelHost {
3737
*/
3838
fun addLibraries(libraries: Collection<LibraryDefinition>)
3939

40+
/**
41+
* Says whether this [typeName] should be loaded as integration based on loaded libraries.
42+
* `null` means that loaded libraries don't care about this [typeName].
43+
*/
44+
fun acceptsIntegrationTypeName(typeName: String): Boolean?
45+
4046
/**
4147
* Loads Kotlin standard artifacts (org.jetbrains.kotlin:kotlin-$name:$version)
4248
*

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

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,11 @@ import org.jetbrains.kotlinx.jupyter.api.ResultHandlerExecution
2222
import org.jetbrains.kotlinx.jupyter.api.SubtypeRendererTypeHandler
2323
import org.jetbrains.kotlinx.jupyter.api.SubtypeThrowableRenderer
2424
import org.jetbrains.kotlinx.jupyter.api.ThrowableRenderer
25+
import org.jetbrains.kotlinx.jupyter.api.TypeName
2526
import org.jetbrains.kotlinx.jupyter.api.VariableDeclarationCallback
2627
import org.jetbrains.kotlinx.jupyter.api.VariableUpdateCallback
28+
import org.jetbrains.kotlinx.jupyter.util.AcceptanceRule
29+
import org.jetbrains.kotlinx.jupyter.util.NameAcceptanceRule
2730
import kotlin.reflect.KMutableProperty
2831

2932
/**
@@ -66,6 +69,8 @@ abstract class JupyterIntegration : LibraryDefinitionProducer {
6669

6770
private val internalVariablesMarkers = mutableListOf<InternalVariablesMarker>()
6871

72+
private val integrationTypeNameRules = mutableListOf<AcceptanceRule<String>>()
73+
6974
fun addRenderer(handler: RendererHandler) {
7075
renderers.add(handler)
7176
}
@@ -197,6 +202,31 @@ abstract class JupyterIntegration : LibraryDefinitionProducer {
197202
preprocessCodeWithLibraries { CodePreprocessor.Result(this.callback(it)) }
198203
}
199204

205+
/**
206+
* All integrations transitively loaded by this integration will be tested against
207+
* passed acceptance rule and won't be loaded if the rule returned `false`.
208+
* If there were no acceptance rules that returned not-null values, integration
209+
* **will be loaded**. If there are several acceptance rules that returned not-null values,
210+
* the latest one will be taken into account.
211+
*/
212+
fun addIntegrationTypeNameRule(rule: AcceptanceRule<TypeName>) {
213+
integrationTypeNameRules.add(rule)
214+
}
215+
216+
/**
217+
* See [addIntegrationTypeNameRule]
218+
*/
219+
fun acceptIntegrationTypeNameIf(predicate: (TypeName) -> Boolean) {
220+
addIntegrationTypeNameRule(NameAcceptanceRule(true, predicate))
221+
}
222+
223+
/**
224+
* See [addIntegrationTypeNameRule]
225+
*/
226+
fun discardIntegrationTypeNameIf(predicate: (TypeName) -> Boolean) {
227+
addIntegrationTypeNameRule(NameAcceptanceRule(false, predicate))
228+
}
229+
200230
internal fun getDefinition() =
201231
libraryDefinition {
202232
it.init = init
@@ -214,6 +244,7 @@ abstract class JupyterIntegration : LibraryDefinitionProducer {
214244
it.resources = resources
215245
it.codePreprocessors = codePreprocessors
216246
it.internalVariablesMarkers = internalVariablesMarkers
247+
it.integrationTypeNameRules = integrationTypeNameRules
217248
}
218249
}
219250

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import org.jetbrains.kotlinx.jupyter.api.InternalVariablesMarker
1010
import org.jetbrains.kotlinx.jupyter.api.KotlinKernelVersion
1111
import org.jetbrains.kotlinx.jupyter.api.RendererHandler
1212
import org.jetbrains.kotlinx.jupyter.api.ThrowableRenderer
13+
import org.jetbrains.kotlinx.jupyter.util.AcceptanceRule
1314

1415
/**
1516
* Library definition represents "library" concept in Kotlin kernel.
@@ -117,4 +118,10 @@ interface LibraryDefinition {
117118
*/
118119
val internalVariablesMarkers: List<InternalVariablesMarker>
119120
get() = emptyList()
121+
122+
/**
123+
* Integration type name rules for the library integration classes which are about to be loaded transitively
124+
*/
125+
val integrationTypeNameRules: List<AcceptanceRule<String>>
126+
get() = emptyList()
120127
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import org.jetbrains.kotlinx.jupyter.api.InternalVariablesMarker
1010
import org.jetbrains.kotlinx.jupyter.api.KotlinKernelVersion
1111
import org.jetbrains.kotlinx.jupyter.api.RendererHandler
1212
import org.jetbrains.kotlinx.jupyter.api.ThrowableRenderer
13+
import org.jetbrains.kotlinx.jupyter.util.AcceptanceRule
1314

1415
/**
1516
* Trivial implementation of [LibraryDefinition] - simple container.
@@ -32,6 +33,7 @@ class LibraryDefinitionImpl private constructor() : LibraryDefinition {
3233
override var internalVariablesMarkers: List<InternalVariablesMarker> = emptyList()
3334
override var minKernelVersion: KotlinKernelVersion? = null
3435
override var originalDescriptorText: String? = null
36+
override var integrationTypeNameRules: List<AcceptanceRule<String>> = emptyList()
3537

3638
companion object {
3739
internal fun build(buildAction: (LibraryDefinitionImpl) -> Unit): LibraryDefinition {
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package org.jetbrains.kotlinx.jupyter.util
2+
3+
import kotlinx.serialization.Serializable
4+
import org.jetbrains.kotlinx.jupyter.api.TypeName
5+
import org.jetbrains.kotlinx.jupyter.api.libraries.VariablesSubstitutionAware
6+
7+
/**
8+
* Acceptance rule either says it accepts object or delegates it to some other rule returning null
9+
*/
10+
fun interface AcceptanceRule<T> {
11+
fun accepts(obj: T): Boolean?
12+
}
13+
14+
/**
15+
* Acceptance rule that has only two answers: yes/no (depending on the [acceptsFlag]) and "don't know"
16+
*/
17+
interface FlagAcceptanceRule<T> : AcceptanceRule<T> {
18+
val acceptsFlag: Boolean
19+
fun appliesTo(obj: T): Boolean
20+
21+
override fun accepts(obj: T): Boolean? {
22+
return if (appliesTo(obj)) acceptsFlag else null
23+
}
24+
}
25+
26+
/**
27+
* Acceptance rule for type names
28+
*/
29+
class NameAcceptanceRule(
30+
override val acceptsFlag: Boolean,
31+
private val appliesPredicate: (TypeName) -> Boolean
32+
) : FlagAcceptanceRule<TypeName> {
33+
override fun appliesTo(obj: TypeName): Boolean {
34+
return appliesPredicate(obj)
35+
}
36+
}
37+
38+
/**
39+
* Acceptance rule for type names based on [pattern].
40+
* Pattern may consist of any characters and of 3 special combinations:
41+
* 1) `?` - any single character or no character
42+
* 2) `*` - any character sequence excluding dot (`.`)
43+
* 3) `**` - any character sequence
44+
*
45+
* For example, pattern `org.jetbrains.kotlin?.**.jupyter.*` matches following names:
46+
* - `org.jetbrains.kotlin.my.package.jupyter.Integration`
47+
* - `org.jetbrains.kotlinx.some_package.jupyter.SomeClass`
48+
*
49+
* It doesn't match name `org.jetbrains.kotlin.my.package.jupyter.integration.MyClass`
50+
*/
51+
@Serializable(PatternNameAcceptanceRuleSerializer::class)
52+
class PatternNameAcceptanceRule(
53+
override val acceptsFlag: Boolean,
54+
val pattern: String,
55+
) : FlagAcceptanceRule<TypeName>, VariablesSubstitutionAware<PatternNameAcceptanceRule> {
56+
private val regex by lazy {
57+
buildString {
58+
var i = 0
59+
while (i < pattern.length) {
60+
val c = pattern[i]
61+
val nextC = pattern.getOrNull(i + 1)
62+
63+
when (c) {
64+
'.' -> append("\\.")
65+
'*' -> when (nextC) {
66+
'*' -> {
67+
append(".*")
68+
++i
69+
}
70+
else -> append("[^.]*")
71+
}
72+
'?' -> append(".?")
73+
'[', ']', '(', ')', '{', '}', '\\', '$', '^', '+', '|' -> {
74+
append('\\')
75+
append(c)
76+
}
77+
else -> append(c)
78+
}
79+
++i
80+
}
81+
}.toRegex()
82+
}
83+
84+
override fun appliesTo(obj: TypeName): Boolean {
85+
return regex.matches(obj)
86+
}
87+
88+
override fun replaceVariables(mapping: Map<String, String>): PatternNameAcceptanceRule {
89+
val newPattern = replaceVariables(pattern, mapping)
90+
if (pattern == newPattern) return this
91+
return PatternNameAcceptanceRule(acceptsFlag, newPattern)
92+
}
93+
}
94+
95+
/**
96+
* List of acceptance rules:
97+
* 1) accepts [obj] if latest not-null acceptance result is `true`
98+
* 2) doesn't accept [obj] if latest not-null acceptance result is `false`
99+
* 3) returns `null` if all acceptance results are `null` or the iterable is empty
100+
*/
101+
fun <T> Iterable<AcceptanceRule<T>>.accepts(obj: T): Boolean? {
102+
return mapNotNull { it.accepts(obj) }.lastOrNull()
103+
}

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

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ abstract class ListToMapSerializer<T, K, V>(
6464
}
6565

6666
override fun serialize(encoder: Encoder, value: List<T>) {
67-
val tempMap = value.map(reverseMapper).toMap()
67+
val tempMap = value.associate(reverseMapper)
6868
utilSerializer.serialize(encoder, tempMap)
6969
}
7070
}
@@ -115,3 +115,30 @@ object ResourceBunchSerializer : KSerializer<ResourceFallbacksBundle> {
115115
encoder.encodeSerializableValue(serializer(), value.locations)
116116
}
117117
}
118+
119+
object PatternNameAcceptanceRuleSerializer : KSerializer<PatternNameAcceptanceRule> {
120+
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor(PatternNameAcceptanceRule::class.qualifiedName!!, PrimitiveKind.STRING)
121+
122+
override fun deserialize(decoder: Decoder): PatternNameAcceptanceRule {
123+
val rule = decoder.decodeString()
124+
fun throwError(): Nothing = throw SerializationException("Wrong format of pattern rule: $rule")
125+
126+
val parts = rule.split(':').map { it.trim() }
127+
val (sign, pattern) = when (parts.size) {
128+
1 -> "+" to parts[0]
129+
2 -> parts[0] to parts[1]
130+
else -> throwError()
131+
}
132+
val accepts = when (sign) {
133+
"+" -> true
134+
"-" -> false
135+
else -> throwError()
136+
}
137+
138+
return PatternNameAcceptanceRule(accepts, pattern)
139+
}
140+
141+
override fun serialize(encoder: Encoder, value: PatternNameAcceptanceRule) {
142+
encoder.encodeString("${ if (value.acceptsFlag) '+' else '-' }:${value.pattern}")
143+
}
144+
}

jupyter-lib/shared-compiler/src/main/kotlin/org/jetbrains/kotlinx/jupyter/libraries/LibraryDescriptor.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import org.jetbrains.kotlinx.jupyter.api.libraries.libraryDefinition
1515
import org.jetbrains.kotlinx.jupyter.config.currentKernelVersion
1616
import org.jetbrains.kotlinx.jupyter.exceptions.ReplPreprocessingException
1717
import org.jetbrains.kotlinx.jupyter.util.KotlinKernelVersionSerializer
18+
import org.jetbrains.kotlinx.jupyter.util.PatternNameAcceptanceRule
1819
import org.jetbrains.kotlinx.jupyter.util.RenderersSerializer
1920
import org.jetbrains.kotlinx.jupyter.util.replaceVariables
2021

@@ -42,6 +43,8 @@ class LibraryDescriptor(
4243

4344
@Serializable(KotlinKernelVersionSerializer::class)
4445
val minKernelVersion: KotlinKernelVersion? = null,
46+
47+
val integrationTypeNameRules: List<PatternNameAcceptanceRule> = emptyList(),
4548
) {
4649
fun convertToDefinition(arguments: List<Variable>): LibraryDefinition {
4750
val mapping = substituteArguments(variables, arguments)
@@ -90,6 +93,7 @@ class LibraryDescriptor(
9093
it.renderers = renderers.replaceVariables(mapping)
9194
it.resources = resources.replaceVariables(mapping)
9295
it.minKernelVersion = minKernelVersion
96+
it.integrationTypeNameRules = integrationTypeNameRules.replaceVariables(mapping)
9397
it.originalDescriptorText = Json.encodeToString(this)
9498
}
9599
}

src/main/kotlin/org/jetbrains/kotlinx/jupyter/libraries/LibrariesScanner.kt

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,20 +19,33 @@ import org.jetbrains.kotlinx.jupyter.exceptions.ReplException
1919

2020
class LibrariesScanner(val notebook: Notebook) {
2121
private val processedFQNs = mutableSetOf<TypeName>()
22-
23-
private fun <T, I : LibrariesInstantiable<T>> Iterable<I>.filterProcessed(): List<I> {
24-
return filter { processedFQNs.add(it.fqn) }
22+
private val discardedFQNs = mutableSetOf<TypeName>()
23+
24+
private fun <T, I : LibrariesInstantiable<T>> Iterable<I>.filterNamesToLoad(host: KotlinKernelHost): List<I> {
25+
return filter {
26+
val typeName = it.fqn
27+
val acceptance = host.acceptsIntegrationTypeName(typeName)
28+
log.debug("Acceptance result for $typeName: $acceptance")
29+
when (acceptance) {
30+
true -> processedFQNs.add(typeName)
31+
false -> {
32+
discardedFQNs.add(typeName)
33+
false
34+
}
35+
null -> typeName !in discardedFQNs && processedFQNs.add(typeName)
36+
}
37+
}
2538
}
2639

2740
fun addLibrariesFromClassLoader(classLoader: ClassLoader, host: KotlinKernelHost) {
28-
val scanResult = scanForLibraries(classLoader)
41+
val scanResult = scanForLibraries(classLoader, host)
2942
log.debug("Scanning for libraries is done. Detected FQNs: ${Json.encodeToString(scanResult)}")
3043
val libraries = instantiateLibraries(classLoader, scanResult, notebook)
3144
log.debug("Number of detected definitions: ${libraries.size}")
3245
host.addLibraries(libraries)
3346
}
3447

35-
private fun scanForLibraries(classLoader: ClassLoader): LibrariesScanResult {
48+
private fun scanForLibraries(classLoader: ClassLoader, host: KotlinKernelHost): LibrariesScanResult {
3649
val results = classLoader.getResources("$KOTLIN_JUPYTER_RESOURCES_PATH/$KOTLIN_JUPYTER_LIBRARIES_FILE_NAME").toList().map { url ->
3750
val contents = url.readText()
3851
Json.decodeFromString<LibrariesScanResult>(contents)
@@ -47,8 +60,8 @@ class LibrariesScanner(val notebook: Notebook) {
4760
}
4861

4962
return LibrariesScanResult(
50-
definitions.filterProcessed(),
51-
producers.filterProcessed(),
63+
definitions.filterNamesToLoad(host),
64+
producers.filterNamesToLoad(host),
5265
)
5366
}
5467

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package org.jetbrains.kotlinx.jupyter.repl
33
import org.jetbrains.kotlinx.jupyter.api.Code
44
import org.jetbrains.kotlinx.jupyter.api.libraries.ExecutionHost
55
import org.jetbrains.kotlinx.jupyter.messaging.DisplayHandler
6+
import org.jetbrains.kotlinx.jupyter.repl.impl.ExecutionStackFrame
67

78
/**
89
* Executes notebook cell code.
@@ -18,7 +19,8 @@ interface CellExecutor : ExecutionHost {
1819
processMagics: Boolean = true,
1920
invokeAfterCallbacks: Boolean = true,
2021
currentCellId: Int = -1,
21-
callback: ExecutionStartedCallback? = null
22+
stackFrame: ExecutionStackFrame? = null,
23+
callback: ExecutionStartedCallback? = null,
2224
): InternalEvalResult
2325
}
2426

0 commit comments

Comments
 (0)