Kotlin Kernel for Jupyter notebooks

repository·master·Indexed 22 days ago

https://github.com/kotlin/kotlin-jupyter

The Kotlin Kernel allows developers to run Kotlin code interactively within Jupyter environments including JupyterLab, Jupyter Notebook, Datalore, and the Kotlin Notebook plugin for IntelliJ IDEA. It supports dynamic dependency management via @file:DependsOn, @file:Repository, and Gradle-like USE blocks, as well as line magics like %use for importing integrated libraries. The kernel is compatible with Windows, Ubuntu Linux, and macOS.

Tokens
34.5K
Snippets
72
Records
122
Agent score
78%

What's inside kotlin-jupyter

  1. Overview of Kotlin Kernel for Jupyter notebooks

    master

    The Kotlin Kernel for Jupyter notebooks is a tool that enables writing and running Kotlin code within the Jupyter Notebook environment. It acts as a bridge between Jupyter and the Kotlin compiler, providing interactive features such as:

    • Immediate output from code cells.
    • Basic code completion and error analysis.
    • Access to APIs within cells and for handling outputs.
    • Ability to retrieve information from previously executed snippets.
    • Easy library importing and integration.

    You can use the Kotlin Kernel in IntelliJ IDEA (via the Kotlin Notebook plugin), Datalore (natively supported), or standard Jupyter clients (JupyterLab, Jupyter Notebook, Jupyter Console).

  2. Explore Kotlin Jupyter documentation

    master

    The project provides several documentation resources:

    • Docs site: Contains rendered KDoc comments from the codebase.
    • Library Integration Docs: Guidance on integrating new libraries. Library authors should note:
      • The api module is relevant for library integration.
      • The lib module contains entities available from Notebook cells.
      • The intellij-dependencies-shared module is used for Jupyter REPL integration into standalone applications or IntelliJ IDEA plugins.
    • Kotlin Notebook Docs: Information on features, use cases, and tutorials for the Kotlin Notebook plugin.
  3. Integrate new libraries into Kotlin Kernel

    master

    There are two primary methods to integrate a library into the Kotlin Kernel for Jupyter notebooks:

    1. Creating a JSON library descriptor: An easy solution that does not require modifying the library itself. You create a JSON file that defines features like properties, renderers, and initial imports. Libraries integrated this way can be loaded using the %use line magic.

    2. Using the Kotlin API: This method involves adding integration logic directly to the library code (or a separate integration project). The library is automatically integrated when its JAR containing a META-INF/kotlin-jupyter-libraries/libraries.json file (specifying the integration class name) is added to the notebook classpath. You can trigger this integration using the @file:DependsOn annotation or via a JSON descriptor that defines the dependency.

  4. Library integration methods overview

    master

    When deciding how to integrate a library, choose based on your level of control over the library source:

    MethodRequires Library Changes?Mechanism
    JSON library descriptorNoA JSON file defining properties, renderers, and imports. Loaded via %use magic.
    Kotlin APIYes (or separate integration project)An integration class defined in code. Automatically detected via META-INF/kotlin-jupyter-libraries/libraries.json in the JAR.
  5. Understand the purpose of the jupyter-lib/protocol module

    master

    The jupyter-lib/protocol module provides the core protocol logic and data structures required for Jupyter kernel communication. It is designed to be a lightweight module with minimal dependencies, making it suitable for import into other projects without requiring the Kotlin compiler or other kernel-specific logic.

    It contains the org.jetbrains.kotlinx.jupyter.protocol package and parts of org.jetbrains.kotlinx.jupyter.messaging.

  6. Implement library callbacks

    master

    Libraries can hook into the notebook lifecycle using various callback mechanisms.

    Common Lifecycle Hooks:

    • After loading (once): init (Descriptor) or onLoaded {} (JupyterIntegration). Use scheduleExecution("code") inside onLoaded to make variables visible in the notebook.
    • Before each cell execution: initCell (Descriptor) or beforeCellExecution {} (JupyterIntegration).
    • After each cell execution: afterCellExecution {} (JupyterIntegration only). Provides access to snippetInstance and resultField.
    • On cell interruption: onInterrupt {} (JupyterIntegration only).
    • Before kernel shutdown: shutdown (Descriptor) or onShutdown {} (JupyterIntegration).
    • On color scheme change: onColorSchemeChange {} (JupyterIntegration only).
    // Example: Callback after loading
    USE {
        onLoaded { 
            println("Integration loaded") 
            scheduleExecution("val x = 3") 
        }
    }
    
    // Example: Callback after cell execution
    USE {
        afterCellExecution { snippetInstance, resultField -> 
            println("After cell execution: ${resultField.name} = ${resultField.value}") 
        }
    }
  7. Understand Kotlin kernel compatibility requirements

    master
    The Kotlin kernel's functionality depends on specific versions of underlying libraries, most notably Kotlin scripting and KSP (Kotlin Symbol Processing). Because KSP requires specific versions of Kotlin to function, you must ensure that the version of the Kotlin kernel you are using is compatible with your environment's Kotlin version and the libraries it relies on.
  8. How rendering works in the Kotlin Kernel

    master

    Rendering is the process of transforming a value into a displayable format for the Jupyter client. The kernel follows a specific hierarchy:

    1. Renderers: The kernel first attempts to use RenderersProcessor to convert a value into a Renderable or DisplayResult object. If successful, the result is converted to JSON via toJson().
    2. Text Rendering: If no Renderable is found, the TextRenderersProcessor attempts to find a text renderer that returns a non-null string. If all fail, the kernel falls back to the object's .toString() method wrapped in text/plain.
    3. Throwable Rendering: If a cell execution fails, the kernel uses Throwable renderers to display the exception. If none are available, the error is printed to standard error.

    Libraries can define custom renderers to handle specific types (like charts or data frames) during any of these stages.

  9. Methods to integrate new libraries

    master

    There are three primary ways to integrate library features into a Kotlin Kernel notebook:

    1. Descriptor API: Create a JSON file containing feature descriptions. This file is loaded using the %use line magic. You can also load a JSON string directly using loadLibraryDescriptor inside a notebook cell.
    2. JupyterIntegration API: Add features directly from a notebook cell using the USE { ... } block. This is the most common programmatic method.
    3. LibraryDefinition API: Use a LibraryDefinition instance (created via libraryDefinition { ... }) passed to the USE() function. This is typically used when building a Kotlin JVM library to provide integration logic.
  10. Create a library descriptor for %use magic

    master

    To make a JVM library available via the %use <libName> magic in Kotlin notebooks, you must create a <libName>.json library descriptor. This file defines dependencies, imports, and initialization code. All fields are optional.

    Key fields include:

    • properties: Dictionary for internal descriptor variables (referenced as $property).
    • description: Short text for the library list.
    • link: URL displayed via the :help command.
    • minKernelVersion: Minimum required kernel version.
    • repositories: Maven or Ivy repositories.
    • dependencies: List of library dependencies.
    • imports: Default imports to be added.
    • init: Code snippets executed when the library is included.
    • initCell: Code snippets executed before any cell.
    • shutdown: Cleanup code executed on kernel shutdown.
    • renderers: Mapping of FQNs to Kotlin expressions for custom rendering (use $it for the source object).
    • resources: JS/CSS resources.
    • integrationTypeNameRules: Rules for loading integration classes using patterns like [+|-]:<pattern> (where ? is any single character, * is any sequence excluding dot, and ** is any sequence).
  11. Add dynamic dependencies using Gradle-like syntax

    master

    You can load libraries from Maven repositories using a USE { ... } block in any cell. This allows you to specify repositories, credentials, and dependencies using a syntax similar to Gradle.

    Limitations: This is not a full Gradle execution. Gradle metadata is not resolved, and advanced configurations like top-level Multiplatform dependencies are not supported. For Multiplatform libraries, you must manually specify the -jvm variant.

    USE {
    	repositories {
    		maven {
    			url = "https://my.secret.repo/maven/"
    			credentials {
    				username = USER
    				password = TOKEN
    			}
    		}
    	}
    
    	dependencies {
    		val ktorVersion = "2.0.3"
    
    		implementation("my.secret:artifact:1.0-beta")
    		implementation("io.ktor:ktor-client-core:$ktorVersion")
    		implementation("io.ktor:ktor-client-apache:$ktorVersion")
    	}
    }