diff --git a/build.sc b/build.sc index b985834db..9a3748a95 100644 --- a/build.sc +++ b/build.sc @@ -783,7 +783,7 @@ def exampleNotebooks = T.sources { .map(PathRef(_)) } -def validateExamples(matcher: String = "") = { +def validateExamples(matcher: String = "", update: Boolean = false) = { val sv = "2.12.12" val kernelId = "almond-sources-tmp" val baseRepoRoot = os.rel / "out" / "repo" @@ -829,11 +829,9 @@ def validateExamples(matcher: String = "") = { "--install", "--force", "--trap-output", - "--predef-code", - maybeEscapeArg("sys.props(\"almond.ids.random\") = \"0\""), "--extra-repository", s"ivy:${repoRoot.toNIO.toUri.toASCIIString}/[defaultPattern]" - ).call(cwd = examplesDir) + ).call(cwd = examplesDir, env = Map("ALMOND_USE_RANDOM_IDS" -> "false")) val nbFiles = exampleNotebooks() .map(_.path) @@ -846,7 +844,7 @@ def validateExamples(matcher: String = "") = { var errorCount = 0 for (f <- nbFiles) { val output = outputDir / f.last - os.proc( + val res = os.proc( "jupyter", "nbconvert", "--to", @@ -855,38 +853,62 @@ def validateExamples(matcher: String = "") = { s"--ExecutePreprocessor.kernel_name=$kernelId", f, s"--output=$output" - ).call(cwd = examplesDir, env = Map("JUPYTER_PATH" -> jupyterPath.toString)) - - val rawOutput = os.read(output, Charset.defaultCharset()) - - var updatedOutput = rawOutput - if (Properties.isWin) - updatedOutput = updatedOutput.replace("\r\n", "\n").replace("\\r\\n", "\\n") - - // Clear metadata, that usually looks like - // "metadata": { - // "execution": { - // "iopub.execute_input": "2022-08-17T10:35:13.619221Z", - // "iopub.status.busy": "2022-08-17T10:35:13.614065Z", - // "iopub.status.idle": "2022-08-17T10:35:16.310834Z", - // "shell.execute_reply": "2022-08-17T10:35:16.311111Z" - // } - // } - val json = ujson.read(updatedOutput) - for (cell <- json("cells").arr if cell("cell_type").str == "code") - cell("metadata") = ujson.Obj() - updatedOutput = json.render(1) - - // writing the updated notebook on disk for the diff below - os.write.over(output, updatedOutput.getBytes(Charset.defaultCharset())) - - val result = os.read(output, Charset.defaultCharset()) - val expected = os.read(f) - - if (result != expected) { - System.err.println(s"${f.last} differs:") - System.err.println() - os.proc("diff", "-u", f, output).call(cwd = examplesDir) + ).call( + cwd = examplesDir, + env = Map( + "JUPYTER_PATH" -> jupyterPath.toString, + "ALMOND_USE_RANDOM_IDS" -> "false" + ), + check = false + ) + + if (res.exitCode == 0) { + if (!os.exists(output)) { + val otherOutput = output / os.up / s"${output.last.stripSuffix(".ipynb")}.nbconvert.ipynb" + if (os.exists(otherOutput)) + os.move(otherOutput, output) + } + val rawOutput = os.read(output, Charset.defaultCharset()) + + var updatedOutput = rawOutput + if (Properties.isWin) + updatedOutput = updatedOutput.replace("\r\n", "\n").replace("\\r\\n", "\\n") + + // Clear metadata, that usually looks like + // "metadata": { + // "execution": { + // "iopub.execute_input": "2022-08-17T10:35:13.619221Z", + // "iopub.status.busy": "2022-08-17T10:35:13.614065Z", + // "iopub.status.idle": "2022-08-17T10:35:16.310834Z", + // "shell.execute_reply": "2022-08-17T10:35:16.311111Z" + // } + // } + val json = ujson.read(updatedOutput) + for (cell <- json("cells").arr if cell("cell_type").str == "code") + cell("metadata") = ujson.Obj() + updatedOutput = json.render(1) + + // writing the updated notebook on disk for the diff below + os.write.over(output, updatedOutput.getBytes(Charset.defaultCharset())) + + val result = os.read(output, Charset.defaultCharset()) + val expected = os.read(f) + + if (result != expected) { + System.err.println(s"${f.last} differs:") + System.err.println() + os.proc("diff", "-u", f, output) + .call(cwd = examplesDir, check = false, stdin = os.Inherit, stdout = os.Inherit) + if (update) { + System.err.println(s"Updating ${f.last}") + os.copy.over(output, f) + } + else + errorCount += 1 + } + } + else { + System.err.println(s"Failed to run nbconvert for ${f.last}") errorCount += 1 } } diff --git a/examples/colors.ipynb b/examples/colors.ipynb index 2021ce5ff..266d4be64 100644 --- a/examples/colors.ipynb +++ b/examples/colors.ipynb @@ -10,7 +10,7 @@ { "data": { "text/plain": [ - "\u001b[36mres0\u001b[39m: \u001b[32mInt\u001b[39m = \u001b[32m2\u001b[39m" + "\u001b[36mres1\u001b[39m: \u001b[32mInt\u001b[39m = \u001b[32m2\u001b[39m" ] }, "execution_count": 1, @@ -47,7 +47,7 @@ { "data": { "text/plain": [ - "res2: Int = 3" + "res3: Int = 3" ] }, "execution_count": 3, diff --git a/examples/plotly-scala.ipynb b/examples/plotly-scala.ipynb index f024132a2..b5ecd6113 100644 --- a/examples/plotly-scala.ipynb +++ b/examples/plotly-scala.ipynb @@ -171,7 +171,7 @@ "\u001b[36mdata\u001b[39m: \u001b[32mSeq\u001b[39m[\u001b[32mScatter\u001b[39m] = \u001b[33mList\u001b[39m(\n", " \u001b[33mScatter\u001b[39m(\n", "...\n", - "\u001b[36mres1_3\u001b[39m: \u001b[32mString\u001b[39m = \u001b[32m\"plot-1\"\u001b[39m" + "\u001b[36mres2_3\u001b[39m: \u001b[32mString\u001b[39m = \u001b[32m\"plot-1\"\u001b[39m" ] }, "execution_count": 2, @@ -311,7 +311,7 @@ "\u001b[36mlayout\u001b[39m: \u001b[32mLayout\u001b[39m = \u001b[33mLayout\u001b[39m(\n", " \u001b[33mSome\u001b[39m(\u001b[32m\"Line and Scatter Plot\"\u001b[39m),\n", "...\n", - "\u001b[36mres2_5\u001b[39m: \u001b[32mString\u001b[39m = \u001b[32m\"plot-2\"\u001b[39m" + "\u001b[36mres3_5\u001b[39m: \u001b[32mString\u001b[39m = \u001b[32m\"plot-2\"\u001b[39m" ] }, "execution_count": 3, @@ -530,7 +530,7 @@ "\u001b[36mlayout\u001b[39m: \u001b[32mLayout\u001b[39m = \u001b[33mLayout\u001b[39m(\n", " \u001b[33mSome\u001b[39m(\u001b[32m\"Votes cast for ten lowest voting age population in OECD countries\"\u001b[39m),\n", "...\n", - "\u001b[36mres3_7\u001b[39m: \u001b[32mString\u001b[39m = \u001b[32m\"plot-3\"\u001b[39m" + "\u001b[36mres4_7\u001b[39m: \u001b[32mString\u001b[39m = \u001b[32m\"plot-3\"\u001b[39m" ] }, "execution_count": 4, @@ -702,7 +702,7 @@ "\u001b[36mdata\u001b[39m: \u001b[32mSeq\u001b[39m[\u001b[32mBar\u001b[39m] = \u001b[33mList\u001b[39m(\n", " \u001b[33mBar\u001b[39m(\n", "...\n", - "\u001b[36mres4_1\u001b[39m: \u001b[32mString\u001b[39m = \u001b[32m\"plot-4\"\u001b[39m" + "\u001b[36mres5_1\u001b[39m: \u001b[32mString\u001b[39m = \u001b[32m\"plot-4\"\u001b[39m" ] }, "execution_count": 5, @@ -813,7 +813,7 @@ "\u001b[36mlayout\u001b[39m: \u001b[32mLayout\u001b[39m = \u001b[33mLayout\u001b[39m(\n", " \u001b[32mNone\u001b[39m,\n", "...\n", - "\u001b[36mres5_4\u001b[39m: \u001b[32mString\u001b[39m = \u001b[32m\"plot-5\"\u001b[39m" + "\u001b[36mres6_4\u001b[39m: \u001b[32mString\u001b[39m = \u001b[32m\"plot-5\"\u001b[39m" ] }, "execution_count": 6, @@ -960,7 +960,7 @@ "\u001b[36mlayout\u001b[39m: \u001b[32mLayout\u001b[39m = \u001b[33mLayout\u001b[39m(\n", " \u001b[33mSome\u001b[39m(\u001b[32m\"January 2013 Sales Report\"\u001b[39m),\n", "...\n", - "\u001b[36mres6_6\u001b[39m: \u001b[32mString\u001b[39m = \u001b[32m\"plot-6\"\u001b[39m" + "\u001b[36mres7_6\u001b[39m: \u001b[32mString\u001b[39m = \u001b[32m\"plot-6\"\u001b[39m" ] }, "execution_count": 7, @@ -1096,7 +1096,7 @@ "\u001b[36mlayout\u001b[39m: \u001b[32mLayout\u001b[39m = \u001b[33mLayout\u001b[39m(\n", " \u001b[33mSome\u001b[39m(\u001b[32m\"Least Used Feature\"\u001b[39m),\n", "...\n", - "\u001b[36mres7_5\u001b[39m: \u001b[32mString\u001b[39m = \u001b[32m\"plot-7\"\u001b[39m" + "\u001b[36mres8_5\u001b[39m: \u001b[32mString\u001b[39m = \u001b[32m\"plot-7\"\u001b[39m" ] }, "execution_count": 8, @@ -1402,7 +1402,7 @@ "\u001b[36mlayout\u001b[39m: \u001b[32mLayout\u001b[39m = \u001b[33mLayout\u001b[39m(\n", " \u001b[33mSome\u001b[39m(\u001b[32m\"Annual Profit 2015\"\u001b[39m),\n", "...\n", - "\u001b[36mres8_10\u001b[39m: \u001b[32mString\u001b[39m = \u001b[32m\"plot-8\"\u001b[39m" + "\u001b[36mres9_10\u001b[39m: \u001b[32mString\u001b[39m = \u001b[32m\"plot-8\"\u001b[39m" ] }, "execution_count": 9, @@ -1577,7 +1577,7 @@ "\u001b[36mdata\u001b[39m: \u001b[32mSeq\u001b[39m[\u001b[32mBar\u001b[39m] = \u001b[33mList\u001b[39m(\n", " \u001b[33mBar\u001b[39m(\n", "...\n", - "\u001b[36mres9_1\u001b[39m: \u001b[32mString\u001b[39m = \u001b[32m\"plot-9\"\u001b[39m" + "\u001b[36mres10_1\u001b[39m: \u001b[32mString\u001b[39m = \u001b[32m\"plot-9\"\u001b[39m" ] }, "execution_count": 10, @@ -1698,7 +1698,7 @@ "\u001b[36mlayout\u001b[39m: \u001b[32mLayout\u001b[39m = \u001b[33mLayout\u001b[39m(\n", " \u001b[33mSome\u001b[39m(\u001b[32m\"Colored Bar Chart\"\u001b[39m),\n", "...\n", - "\u001b[36mres10_4\u001b[39m: \u001b[32mString\u001b[39m = \u001b[32m\"plot-10\"\u001b[39m" + "\u001b[36mres11_4\u001b[39m: \u001b[32mString\u001b[39m = \u001b[32m\"plot-10\"\u001b[39m" ] }, "execution_count": 11, @@ -1807,7 +1807,7 @@ "\u001b[36mdata\u001b[39m: \u001b[32mSeq\u001b[39m[\u001b[32mScatter\u001b[39m] = \u001b[33mList\u001b[39m(\n", " \u001b[33mScatter\u001b[39m(\n", "...\n", - "\u001b[36mres11_1\u001b[39m: \u001b[32mString\u001b[39m = \u001b[32m\"plot-11\"\u001b[39m" + "\u001b[36mres12_1\u001b[39m: \u001b[32mString\u001b[39m = \u001b[32m\"plot-11\"\u001b[39m" ] }, "execution_count": 12, @@ -1926,7 +1926,7 @@ "\u001b[36mlayout\u001b[39m: \u001b[32mLayout\u001b[39m = \u001b[33mLayout\u001b[39m(\n", " \u001b[33mSome\u001b[39m(\u001b[32m\"Bubble Chart Hover Text\"\u001b[39m),\n", "...\n", - "\u001b[36mres12_3\u001b[39m: \u001b[32mString\u001b[39m = \u001b[32m\"plot-12\"\u001b[39m" + "\u001b[36mres13_3\u001b[39m: \u001b[32mString\u001b[39m = \u001b[32m\"plot-12\"\u001b[39m" ] }, "execution_count": 13, @@ -2056,7 +2056,7 @@ "\u001b[36mdata\u001b[39m: \u001b[32mSeq\u001b[39m[\u001b[32mScatter\u001b[39m] = \u001b[33mList\u001b[39m(\n", " \u001b[33mScatter\u001b[39m(\n", "...\n", - "\u001b[36mres13_3\u001b[39m: \u001b[32mString\u001b[39m = \u001b[32m\"plot-13\"\u001b[39m" + "\u001b[36mres14_3\u001b[39m: \u001b[32mString\u001b[39m = \u001b[32m\"plot-13\"\u001b[39m" ] }, "execution_count": 14, diff --git a/examples/test.ipynb b/examples/test.ipynb index 5217e1466..54d1f7104 100644 --- a/examples/test.ipynb +++ b/examples/test.ipynb @@ -10,7 +10,7 @@ { "data": { "text/plain": [ - "\u001b[36mres0\u001b[39m: \u001b[32mInt\u001b[39m = \u001b[32m2\u001b[39m" + "\u001b[36mres1\u001b[39m: \u001b[32mInt\u001b[39m = \u001b[32m2\u001b[39m" ] }, "execution_count": 1, diff --git a/modules/scala/integration/src/main/scala/almond/integration/KernelTestsDefinitions.scala b/modules/scala/integration/src/main/scala/almond/integration/KernelTestsDefinitions.scala index f6a4424fa..7715b4f9f 100644 --- a/modules/scala/integration/src/main/scala/almond/integration/KernelTestsDefinitions.scala +++ b/modules/scala/integration/src/main/scala/almond/integration/KernelTestsDefinitions.scala @@ -6,6 +6,8 @@ abstract class KernelTestsDefinitions extends AlmondFunSuite { def kernelLauncher: KernelLauncher + override def mightRetry = true + test("jvm-repr") { kernelLauncher.withKernel { implicit runner => implicit val sessionId: SessionId = SessionId() @@ -64,6 +66,13 @@ abstract class KernelTestsDefinitions extends AlmondFunSuite { } } + test("toree Html") { + kernelLauncher.withKernel { implicit runner => + implicit val sessionId: SessionId = SessionId() + almond.integration.Tests.toreeHtml() + } + } + test("toree AddJar custom protocol") { kernelLauncher.withKernel { implicit runner => implicit val sessionId: SessionId = SessionId() diff --git a/modules/scala/integration/src/test/scala/almond/integration/KernelTestsTwoStepStartup212.scala b/modules/scala/integration/src/test/scala/almond/integration/KernelTestsTwoStepStartup212.scala index 564113e01..ba11b5f45 100644 --- a/modules/scala/integration/src/test/scala/almond/integration/KernelTestsTwoStepStartup212.scala +++ b/modules/scala/integration/src/test/scala/almond/integration/KernelTestsTwoStepStartup212.scala @@ -5,6 +5,4 @@ class KernelTestsTwoStepStartup212 extends KernelTestsDefinitions { lazy val kernelLauncher = new KernelLauncher(KernelLauncher.LauncherType.Jvm, KernelLauncher.testScala212Version) - override def mightRetry = true - } diff --git a/modules/scala/integration/src/test/scala/almond/integration/KernelTestsTwoStepStartup213.scala b/modules/scala/integration/src/test/scala/almond/integration/KernelTestsTwoStepStartup213.scala index e9bbb1157..fc3a5514c 100644 --- a/modules/scala/integration/src/test/scala/almond/integration/KernelTestsTwoStepStartup213.scala +++ b/modules/scala/integration/src/test/scala/almond/integration/KernelTestsTwoStepStartup213.scala @@ -5,6 +5,4 @@ class KernelTestsTwoStepStartup213 extends KernelTestsDefinitions { lazy val kernelLauncher = new KernelLauncher(KernelLauncher.LauncherType.Jvm, KernelLauncher.testScala213Version) - override def mightRetry = true - } diff --git a/modules/scala/integration/src/test/scala/almond/integration/KernelTestsTwoStepStartup3.scala b/modules/scala/integration/src/test/scala/almond/integration/KernelTestsTwoStepStartup3.scala index c01e21eb9..a69d5b593 100644 --- a/modules/scala/integration/src/test/scala/almond/integration/KernelTestsTwoStepStartup3.scala +++ b/modules/scala/integration/src/test/scala/almond/integration/KernelTestsTwoStepStartup3.scala @@ -5,6 +5,4 @@ class KernelTestsTwoStepStartup3 extends KernelTestsDefinitions { lazy val kernelLauncher = new KernelLauncher(KernelLauncher.LauncherType.Jvm, KernelLauncher.testScalaVersion) - override def mightRetry = true - } diff --git a/modules/scala/jupyter-api/src/main/scala/almond/api/JupyterApi.scala b/modules/scala/jupyter-api/src/main/scala/almond/api/JupyterApi.scala index 6ffaafd35..d4faef3b2 100644 --- a/modules/scala/jupyter-api/src/main/scala/almond/api/JupyterApi.scala +++ b/modules/scala/jupyter-api/src/main/scala/almond/api/JupyterApi.scala @@ -1,10 +1,11 @@ package almond.api -import java.util.UUID - import almond.interpreter.api.{CommHandler, DisplayData, OutputHandler} import jupyter.{Displayer, Displayers} +import java.util.concurrent.atomic.AtomicInteger +import java.util.{Locale, UUID} + import scala.reflect.{ClassTag, classTag} abstract class JupyterApi { api => @@ -107,6 +108,13 @@ object JupyterApi { case object Exit extends ExecuteHookResult } + private lazy val useRandomIds: Boolean = + Option(System.getenv("ALMOND_USE_RANDOM_IDS")) + .orElse(sys.props.get("almond.ids.random")) + .forall(s => s == "1" || s.toLowerCase(Locale.ROOT) == "true") + + private val updatableIdCounter = new AtomicInteger(1111111) + abstract class UpdatableResults { @deprecated("Use updatable instead", "0.4.1") @@ -120,7 +128,11 @@ object JupyterApi { // temporary dummy implementation for binary compatibility } def updatable(v: String): String = { - val id = UUID.randomUUID().toString + val id = + if (useRandomIds) + UUID.randomUUID().toString + else + updatableIdCounter.incrementAndGet().toString updatable(id, v) id } diff --git a/modules/scala/jupyter-api/src/main/scala/almond/display/UpdatableDisplay.scala b/modules/scala/jupyter-api/src/main/scala/almond/display/UpdatableDisplay.scala index 3f3bc992e..78260902f 100644 --- a/modules/scala/jupyter-api/src/main/scala/almond/display/UpdatableDisplay.scala +++ b/modules/scala/jupyter-api/src/main/scala/almond/display/UpdatableDisplay.scala @@ -23,11 +23,14 @@ trait UpdatableDisplay extends Display { object UpdatableDisplay { - def useRandomIds(): Boolean = - sys.props - .get("almond.ids.random") + private lazy val useRandomIds0: Boolean = + Option(System.getenv("ALMOND_USE_RANDOM_IDS")) + .orElse(sys.props.get("almond.ids.random")) .forall(s => s == "1" || s.toLowerCase(Locale.ROOT) == "true") + def useRandomIds(): Boolean = + useRandomIds0 + private val idCounter = new AtomicInteger private val divCounter = new AtomicInteger diff --git a/modules/scala/launcher/src/main/scala/almond/launcher/Launcher.scala b/modules/scala/launcher/src/main/scala/almond/launcher/Launcher.scala index 8d18badea..103500dfd 100644 --- a/modules/scala/launcher/src/main/scala/almond/launcher/Launcher.scala +++ b/modules/scala/launcher/src/main/scala/almond/launcher/Launcher.scala @@ -4,7 +4,7 @@ import almond.channels.{Channel, Connection, Message => RawMessage} import almond.channels.zeromq.ZeromqThreads import almond.cslogger.NotebookCacheLogger import almond.interpreter.ExecuteResult -import almond.interpreter.api.OutputHandler +import almond.interpreter.api.{DisplayData, OutputHandler} import almond.kernel.install.Install import almond.kernel.{Kernel, KernelThreads, MessageFile} import almond.logger.{Level, LoggerContext} @@ -367,7 +367,17 @@ object Launcher extends CaseApp[LauncherOptions] { for (outputHandler <- outputHandlerOpt) { val toPrint = s"Launching Scala $scalaVersion kernel" + jvmOpt.fold("")(jvm => s" with JVM $jvm") - outputHandler.stdout(toPrint + System.lineSeparator()) + val toPrintHtml = + s"Launching Scala $scalaVersion kernel" + + jvmOpt.fold("")(jvm => s" with JVM $jvm") + outputHandler.display( + DisplayData( + Map( + DisplayData.ContentType.text -> toPrint, + DisplayData.ContentType.html -> toPrintHtml + ) + ) + ) } Right(actualKernelCommand0) diff --git a/modules/scala/launcher/src/main/scala/almond/launcher/LauncherOptions.scala b/modules/scala/launcher/src/main/scala/almond/launcher/LauncherOptions.scala index 63cdf915d..30cdb1d96 100644 --- a/modules/scala/launcher/src/main/scala/almond/launcher/LauncherOptions.scala +++ b/modules/scala/launcher/src/main/scala/almond/launcher/LauncherOptions.scala @@ -14,6 +14,8 @@ final case class LauncherOptions( connectionFile: Option[String] = None, variableInspector: Option[Boolean] = None, toreeMagics: Option[Boolean] = None, + toreeApi: Option[Boolean] = None, + toreeCompatibility: Option[Boolean] = None, color: Option[Boolean] = None, @HelpMessage("Send log to a file rather than stderr") @ValueDescription("/path/to/log-file") @@ -41,6 +43,10 @@ final case class LauncherOptions( b ++= Seq(s"--variable-inspector=$value") for (value <- toreeMagics) b ++= Seq(s"--toree-magics=$value") + for (value <- toreeApi) + b ++= Seq(s"--toree-api=$value") + for (value <- toreeCompatibility) + b ++= Seq(s"--toree-compatibility=$value") for (value <- color) b ++= Seq(s"--color=$value") for (value <- logTo) diff --git a/modules/scala/scala-interpreter/src/main/scala/almond/Execute.scala b/modules/scala/scala-interpreter/src/main/scala/almond/Execute.scala index 5946965b8..e96fcbc16 100644 --- a/modules/scala/scala-interpreter/src/main/scala/almond/Execute.scala +++ b/modules/scala/scala-interpreter/src/main/scala/almond/Execute.scala @@ -269,7 +269,7 @@ final class Execute( val r = ammInterp.processLine( code0, stmts, - if (storeHistory) currentLine0 else currentNoHistoryLine0, + (if (storeHistory) currentLine0 else currentNoHistoryLine0) + 1, silent = silent(), incrementLine = if (storeHistory) diff --git a/modules/scala/scala-interpreter/src/main/scala/almond/ReplApiImpl.scala b/modules/scala/scala-interpreter/src/main/scala/almond/ReplApiImpl.scala index d64e4e1b2..e0f529295 100644 --- a/modules/scala/scala-interpreter/src/main/scala/almond/ReplApiImpl.scala +++ b/modules/scala/scala-interpreter/src/main/scala/almond/ReplApiImpl.scala @@ -185,7 +185,7 @@ final class ReplApiImpl( def apply(line: String) = ammInterp.processExec( line, - execute0.currentLine, + execute0.currentLine + 1, () => execute0.incrementLineCount() ) match { case Res.Failure(s) => throw new CompilationError(s) diff --git a/modules/scala/scala-interpreter/src/main/scala/almond/ScalaInterpreter.scala b/modules/scala/scala-interpreter/src/main/scala/almond/ScalaInterpreter.scala index b977844a4..e727456c5 100644 --- a/modules/scala/scala-interpreter/src/main/scala/almond/ScalaInterpreter.scala +++ b/modules/scala/scala-interpreter/src/main/scala/almond/ScalaInterpreter.scala @@ -129,7 +129,8 @@ final class ScalaInterpreter( logCtx, jupyterApi.VariableInspector.enabled, outputDir = params.outputDir, - compileOnly = params.compileOnly + compileOnly = params.compileOnly, + addToreeApiCompatibilityImport = params.toreeApiCompatibility ) } diff --git a/modules/scala/scala-interpreter/src/main/scala/almond/ScalaInterpreterParams.scala b/modules/scala/scala-interpreter/src/main/scala/almond/ScalaInterpreterParams.scala index b7c5ef63a..9e43c6e85 100644 --- a/modules/scala/scala-interpreter/src/main/scala/almond/ScalaInterpreterParams.scala +++ b/modules/scala/scala-interpreter/src/main/scala/almond/ScalaInterpreterParams.scala @@ -38,6 +38,7 @@ final case class ScalaInterpreterParams( useThreadInterrupt: Boolean = false, outputDir: Either[os.Path, Boolean] = Right(true), toreeMagics: Boolean = false, + toreeApiCompatibility: Boolean = false, compileOnly: Boolean = false, extraClassPath: List[os.Path] = Nil, initialCellCount: Int = 0 diff --git a/modules/scala/scala-interpreter/src/main/scala/almond/amm/AmmInterpreter.scala b/modules/scala/scala-interpreter/src/main/scala/almond/amm/AmmInterpreter.scala index d834e45ac..19b49cc07 100644 --- a/modules/scala/scala-interpreter/src/main/scala/almond/amm/AmmInterpreter.scala +++ b/modules/scala/scala-interpreter/src/main/scala/almond/amm/AmmInterpreter.scala @@ -53,6 +53,10 @@ object AmmInterpreter { ImportData("almond.input.Input") ) + private def toreeApiCompatibilityImports = Imports( + ImportData("almond.toree.ToreeCompatibility.KernelToreeOps") + ) + /** Instantiate an [[ammonite.interp.Interpreter]] to be used from [[ScalaInterpreter]]. */ def apply( @@ -77,7 +81,8 @@ object AmmInterpreter { logCtx: LoggerContext, variableInspectorEnabled: () => Boolean, outputDir: Either[os.Path, Boolean], - compileOnly: Boolean + compileOnly: Boolean, + addToreeApiCompatibilityImport: Boolean ): ammonite.interp.Interpreter = { val automaticDependenciesMatchers = automaticDependencies @@ -108,6 +113,22 @@ object AmmInterpreter { try { + val addToreeApiCompatibilityImport0 = + addToreeApiCompatibilityImport && { + val loader = frames0().head.classloader + val clsOpt = + try Some(loader.loadClass("almond.toree.ToreeCompatibility$")) + catch { + case _: ClassNotFoundException => + None + } + if (clsOpt.isEmpty) + log.error( + "Ignoring Toree API compatibility option, as sh.almond::toree-hooks isn't part of the user class path" + ) + clsOpt.nonEmpty + } + log.info("Creating Ammonite interpreter") val interpParams = ammonite.interp.Interpreter.Parameters( @@ -138,6 +159,8 @@ object AmmInterpreter { scriptCodeWrapper = codeWrapper, parameters = interpParams ) { + override def wrapperNamePrefix = "cell" + override val compilerManager = new AlmondCompilerLifecycleManager( storage0.dirOpt.map(_.toNIO), headFrame, @@ -182,7 +205,8 @@ object AmmInterpreter { val imports = ammonite.main.Defaults.replImports ++ ammonite.interp.Interpreter.predefImports ++ - almondImports + almondImports ++ + (if (addToreeApiCompatibilityImport0) toreeApiCompatibilityImports else Imports()) for ((e, _) <- ammInterp0.initializePredef(Nil, customPredefs, extraBridges, imports)) e match { case Res.Failure(msg) => diff --git a/modules/scala/scala-interpreter/src/test/scala/almond/EvaluatorTests.scala b/modules/scala/scala-interpreter/src/test/scala/almond/EvaluatorTests.scala index 92428258b..f56ff682a 100644 --- a/modules/scala/scala-interpreter/src/test/scala/almond/EvaluatorTests.scala +++ b/modules/scala/scala-interpreter/src/test/scala/almond/EvaluatorTests.scala @@ -43,20 +43,20 @@ object EvaluatorTests extends TestSuite { runner.run( Seq( ";1; 2L; '3';" -> - """res0_0: Int = 1 - |res0_1: Long = 2L - |res0_2: Char = '3'""".stripMargin, + """res1_0: Int = 1 + |res1_1: Long = 2L + |res1_2: Char = '3'""".stripMargin, "val x = 1; x;" -> """x: Int = 1 - |res1_1: Int = 1""".stripMargin, + |res2_1: Int = 1""".stripMargin, "var x = 1; x = 2; x" -> """x: Int = 2 - |res2_2: Int = 2""".stripMargin, + |res3_2: Int = 2""".stripMargin, "var y = 1; case class C(i: Int = 0){ def foo = x + y }; new C().foo" -> """y: Int = 1 |defined class C - |res3_2: Int = 3""".stripMargin, - "C()" -> (if (isScala212) "res4: C = C(0)" else "res4: C = C(i = 0)") + |res4_2: Int = 3""".stripMargin, + "C()" -> (if (isScala212) "res5: C = C(0)" else "res5: C = C(i = 0)") ) ) } @@ -65,13 +65,13 @@ object EvaluatorTests extends TestSuite { runner.run( Seq( "lazy val x = 'h'" -> (if (TestUtil.isScala2) "" else "x: Char = "), - "x" -> "res1: Char = 'h'", + "x" -> "res2: Char = 'h'", "var w = 'l'" -> ifNotVarUpdates("w: Char = 'l'"), "lazy val y = {w = 'a'; 'A'}" -> (if (TestUtil.isScala2) "" else "y: Char = "), "lazy val z = {w = 'b'; 'B'}" -> (if (TestUtil.isScala2) "" else "z: Char = "), - "z" -> "res5: Char = 'B'", - "y" -> "res6: Char = 'A'", - "w" -> "res7: Char = 'a'" + "z" -> "res6: Char = 'B'", + "y" -> "res7: Char = 'A'", + "w" -> "res8: Char = 'a'" ), Seq( if (TestUtil.isScala2) "x: Char = [lazy]" else "", @@ -91,9 +91,9 @@ object EvaluatorTests extends TestSuite { runner.run( Seq( "var x: Int = 10" -> ifNotVarUpdates("x: Int = 10"), - "x" -> "res1: Int = 10", + "x" -> "res2: Int = 10", "x = 1" -> "", - "x" -> "res3: Int = 1" + "x" -> "res4: Int = 1" ), Seq( ifVarUpdates("x: Int = 10"), diff --git a/modules/scala/scala-interpreter/src/test/scala/almond/ScalaInterpreterTests.scala b/modules/scala/scala-interpreter/src/test/scala/almond/ScalaInterpreterTests.scala index 5cea5cb52..fb376e876 100644 --- a/modules/scala/scala-interpreter/src/test/scala/almond/ScalaInterpreterTests.scala +++ b/modules/scala/scala-interpreter/src/test/scala/almond/ScalaInterpreterTests.scala @@ -176,7 +176,7 @@ object ScalaInterpreterTests extends TestSuite { val textOpt = interpreter.execute("3") .asSuccess .flatMap(_.data.data.get("text/plain")) - val expectedTextOpt = Option("res0: Int = 3") + val expectedTextOpt = Option("res1: Int = 3") assert(textOpt == expectedTextOpt) } @@ -357,7 +357,7 @@ object ScalaInterpreterTests extends TestSuite { val res0 = i.execute(code0) val res1 = i.execute(code1) val res2 = i.execute(code2) - val res3 = i.execute(code3) + val res4 = i.execute(code3) val expectedRes0 = ExecuteResult.Success(DisplayData.empty) val expectedRes1 = ExecuteResult.Success(DisplayData.empty) @@ -371,7 +371,7 @@ object ScalaInterpreterTests extends TestSuite { assert(res0 == expectedRes0) assert(res1 == expectedRes1) assert(res2 == expectedRes2) - assert(res3 == expectedRes3) + assert(res4 == expectedRes3) } } diff --git a/modules/scala/scala-interpreter/src/test/scala/almond/ScalaKernelTests.scala b/modules/scala/scala-interpreter/src/test/scala/almond/ScalaKernelTests.scala index e244468c0..e0b8e9cc2 100644 --- a/modules/scala/scala-interpreter/src/test/scala/almond/ScalaKernelTests.scala +++ b/modules/scala/scala-interpreter/src/test/scala/almond/ScalaKernelTests.scala @@ -644,34 +644,8 @@ object ScalaKernelTests extends TestSuite { } test("toree Html") { - - val interpreter = new ScalaInterpreter( - params = ScalaInterpreterParams( - initialColors = Colors.BlackWhite, - toreeMagics = true - ), - logCtx = logCtx - ) - - val kernel = Kernel.create(interpreter, interpreterEc, threads, logCtx) - .unsafeRunTimedOrThrow() - implicit val sessionId: Dsl.SessionId = Dsl.SessionId() - - kernel.execute( - """%%html - |

- |Hello - |

- |""".stripMargin, - "", - displaysHtml = Seq( - """

- |Hello - |

- |""".stripMargin - ) - ) + almond.integration.Tests.toreeHtml() } test("toree Truncation") { @@ -705,7 +679,7 @@ object ScalaKernelTests extends TestSuite { ) kernel.execute( "(1 to 200).toVector", - "res0: Vector[Int] = " + (1 to 200).toVector.toString + "res1: Vector[Int] = " + (1 to 200).toVector.toString ) kernel.execute( "%truncation on", @@ -715,7 +689,7 @@ object ScalaKernelTests extends TestSuite { ) kernel.execute( "(1 to 200).toVector", - "res1: Vector[Int] = " + + "res2: Vector[Int] = " + (1 to 38) .toVector .map(" " + _ + "," + "\n") diff --git a/modules/scala/scala-interpreter/src/test/scala/almond/TestUtil.scala b/modules/scala/scala-interpreter/src/test/scala/almond/TestUtil.scala index a93bc8306..39b1121cb 100644 --- a/modules/scala/scala-interpreter/src/test/scala/almond/TestUtil.scala +++ b/modules/scala/scala-interpreter/src/test/scala/almond/TestUtil.scala @@ -99,7 +99,8 @@ object TestUtil { private case class Options( predef: List[String] = Nil, - toreeMagics: Boolean = false + toreeMagics: Boolean = false, + toreeApi: Boolean = false ) private val optionsParser: caseapp.Parser[Options] = caseapp.Parser.derive @@ -125,7 +126,8 @@ object TestUtil { initialColors = Colors.BlackWhite, updateBackgroundVariablesEcOpt = Some(new SequentialExecutionContext), predefFiles = opt.predef.map(Paths.get(_)), - toreeMagics = opt.toreeMagics + toreeMagics = opt.toreeMagics, + toreeApiCompatibility = opt.toreeApi ) }, logCtx = logCtx @@ -361,7 +363,7 @@ object TestUtil { for ((a, b) <- publish0.zip(publish) if a != b) System.err.println(s"Expected $b, got $a") for ( - k <- replies0.keySet.intersect(expectedReplies.keySet) + k <- replies0.keySet.intersect(expectedReplies.keySet).toVector.sorted if replies0.get(k) != expectedReplies.get(k) ) System.err.println(s"At line $k: expected ${expectedReplies(k)}, got ${replies0(k)}") diff --git a/modules/scala/scala-kernel/src/main/scala/almond/Options.scala b/modules/scala/scala-kernel/src/main/scala/almond/Options.scala index 7361457fd..9ac31a07e 100644 --- a/modules/scala/scala-kernel/src/main/scala/almond/Options.scala +++ b/modules/scala/scala-kernel/src/main/scala/almond/Options.scala @@ -82,7 +82,11 @@ final case class Options( tmpOutputDirectory: Option[Boolean] = None, @HelpMessage("Add experimental support for Toree magics") - toreeMagics: Boolean = false, + toreeMagics: Option[Boolean] = None, + @HelpMessage("Add experimental support for Toree API compatibility") + toreeApi: Option[Boolean] = None, + @HelpMessage("Add experimental support for Toree compatibility (magics and API)") + toreeCompatibility: Option[Boolean] = None, @HelpMessage("Enable or disable color cell output upon startup (enabled by default, pass --color=false to disable)") color: Boolean = true, diff --git a/modules/scala/scala-kernel/src/main/scala/almond/ScalaKernel.scala b/modules/scala/scala-kernel/src/main/scala/almond/ScalaKernel.scala index 5776e9e5c..8c8678ea9 100644 --- a/modules/scala/scala-kernel/src/main/scala/almond/ScalaKernel.scala +++ b/modules/scala/scala-kernel/src/main/scala/almond/ScalaKernel.scala @@ -148,7 +148,9 @@ object ScalaKernel extends CaseApp[Options] { options.tmpOutputDirectory .getOrElse(true) // Create tmp output dir by default }, - toreeMagics = options.toreeMagics, + toreeMagics = options.toreeMagics.orElse(options.toreeCompatibility).getOrElse(false), + toreeApiCompatibility = + options.toreeApi.orElse(options.toreeCompatibility).getOrElse(false), compileOnly = options.compileOnly, extraClassPath = options.extraClassPath .filter(_.trim.nonEmpty) diff --git a/modules/scala/test-definitions/src/main/scala/almond/integration/Tests.scala b/modules/scala/test-definitions/src/main/scala/almond/integration/Tests.scala index e2e7322e8..0fdbdc620 100644 --- a/modules/scala/test-definitions/src/main/scala/almond/integration/Tests.scala +++ b/modules/scala/test-definitions/src/main/scala/almond/integration/Tests.scala @@ -349,6 +349,38 @@ object Tests { } } + def toreeHtml()(implicit sessionId: SessionId, runner: Runner): Unit = { + val launcherOptions = + if (runner.differedStartUp) + Seq("--shared-dependencies", "sh.almond::toree-hooks:_") + else + Seq("--shared", "sh.almond::toree-hooks") + runner.withLauncherOptionsSession(launcherOptions: _*)("--toree-magics", "--toree-api") { + implicit session => + + execute( + """%%html + |

+ |Hello + |

+ |""".stripMargin, + "", + displaysHtml = Seq( + """

+ |Hello + |

+ |""".stripMargin + ) + ) + + execute( + """kernel.display.html("

Hello

")""", + "", + displaysHtml = Seq("

Hello

") + ) + } + } + private def java17Cmd(): String = { val isAtLeastJava17 = scala.util.Try(sys.props("java.version").takeWhile(_.isDigit).toInt).toOption.exists(_ >= 17) diff --git a/modules/scala/toree-hooks/src/main/scala/almond/toree/ToreeCompatibility.scala b/modules/scala/toree-hooks/src/main/scala/almond/toree/ToreeCompatibility.scala new file mode 100644 index 000000000..72eb29924 --- /dev/null +++ b/modules/scala/toree-hooks/src/main/scala/almond/toree/ToreeCompatibility.scala @@ -0,0 +1,37 @@ +package almond.toree + +import almond.interpreter.api.DisplayData +import ammonite.interp.api.InterpAPI + +import java.io.{InputStream, PrintStream} +import java.net.URI +import java.nio.file.Paths + +/** Import the members of this object to add source-compatibility for some Toree API calls, such as + * 'kernel.display', or 'kernel.addJars'. + */ +object ToreeCompatibility { + implicit class KernelToreeOps(private val kernel: almond.api.JupyterApi) { + + def display: ToreeDisplayMethodsLike = + new ToreeDisplayMethodsLike { + def content(mimeType: String, data: String) = + kernel.publish.display(DisplayData(Map(mimeType -> data))) + def clear(wait: Boolean = false) = + // no-op, not sure what we're supposed to do hereā€¦ + () + } + + def out: PrintStream = System.out + def err: PrintStream = System.err + def in: InputStream = System.in + + def addJars(uris: URI*)(implicit interp: InterpAPI): Unit = { + val (fileUris, other) = uris.partition(_.getScheme == "file") + for (uri <- other) + System.err.println(s"Warning: ignoring $uri") + val files = fileUris.map(Paths.get(_)).map(os.Path(_, os.pwd)) + interp.load.cp(files) + } + } +} diff --git a/modules/scala/toree-hooks/src/main/scala/almond/toree/ToreeDisplayMethodsLike.scala b/modules/scala/toree-hooks/src/main/scala/almond/toree/ToreeDisplayMethodsLike.scala new file mode 100644 index 000000000..d2c575eb1 --- /dev/null +++ b/modules/scala/toree-hooks/src/main/scala/almond/toree/ToreeDisplayMethodsLike.scala @@ -0,0 +1,8 @@ +package almond.toree + +trait ToreeDisplayMethodsLike { + def content(mimeType: String, data: String): Unit + def html(data: String): Unit = content("text/html", data) + def javascript(data: String): Unit = content("application/javascript", data) + def clear(wait: Boolean = false): Unit +}