diff --git a/build.sc b/build.sc index 987405430..c8641795b 100644 --- a/build.sc +++ b/build.sc @@ -194,6 +194,7 @@ class ScalaInterpreter(val crossScalaVersion: String) extends AlmondModule with shared.interpreter(ScalaVersions.scala3Compat), scala.`coursier-logger`(ScalaVersions.scala3Compat), scala.`scala-kernel-api-helper`(), + scala.`shared-directives`(ScalaVersions.scala3Compat), scala.`toree-hooks`(ScalaVersions.binary(crossScalaVersion)) ) else @@ -201,6 +202,7 @@ class ScalaInterpreter(val crossScalaVersion: String) extends AlmondModule with shared.interpreter(), scala.`coursier-logger`(), scala.`scala-kernel-api`(), + scala.`shared-directives`(), scala.`toree-hooks`(ScalaVersions.binary(crossScalaVersion)) ) def ivyDeps = T { @@ -219,11 +221,19 @@ class ScalaInterpreter(val crossScalaVersion: String) extends AlmondModule with metabrowse ++ Agg( Deps.coursier.withDottyCompat(crossScalaVersion), Deps.coursierApi, + Deps.dependencyInterface, + Deps.directiveHandler, Deps.jansi, Deps.ammoniteCompiler(crossScalaVersion).exclude(("net.java.dev.jna", "jna")), Deps.ammoniteRepl(crossScalaVersion).exclude(("net.java.dev.jna", "jna")) ) } + def scalacOptions = super.scalacOptions() ++ { + val scala213Options = + if (scalaVersion().startsWith("2.13.")) Seq("-Ymacro-annotations") + else Nil + scala213Options + } object test extends Tests with AlmondTestModule { def moduleDeps = { val rx = @@ -369,6 +379,17 @@ class CoursierLogger(val crossScalaVersion: String) extends AlmondModule { ) } +class SharedDirectives(val crossScalaVersion: String) extends AlmondModule { + def supports3 = true + def ivyDeps = super.ivyDeps() ++ Agg( + Deps.directiveHandler, + Deps.jsoniterScalaCore.applyBinaryVersion213_3(scalaVersion()) + ) + def compileIvyDeps = Agg( + Deps.jsoniterScalaMacros + ) +} + trait Launcher extends AlmondSimpleModule with BootstrapLauncher with PropertyFile with Bloop.Module { def supports3 = true @@ -376,12 +397,12 @@ trait Launcher extends AlmondSimpleModule with BootstrapLauncher with PropertyFi def scalaVersion = sv def moduleDeps = Seq( scala.`coursier-logger`(ScalaVersions.scala3Compat), + scala.`shared-directives`(ScalaVersions.scala3Compat), shared.kernel(ScalaVersions.scala3Compat) ) def ivyDeps = Agg( Deps.caseApp, Deps.coursierLauncher, - Deps.directiveHandler, Deps.fansi, Deps.scalaparse ) @@ -476,9 +497,11 @@ object scala extends Module { object `scala-kernel-helper` extends Cross[ScalaKernelHelper](ScalaVersions.all.filter(_.startsWith("3.")): _*) object `coursier-logger` extends Cross[CoursierLogger](ScalaVersions.binaries: _*) - object launcher extends Launcher - object `almond-scalapy` extends Cross[AlmondScalaPy](ScalaVersions.binaries: _*) - object `almond-rx` extends Cross[AlmondRx](ScalaVersions.scala212, ScalaVersions.scala213) + object `shared-directives` + extends Cross[SharedDirectives]("2.12.15" +: ScalaVersions.binaries: _*) + object launcher extends Launcher + object `almond-scalapy` extends Cross[AlmondScalaPy](ScalaVersions.binaries: _*) + object `almond-rx` extends Cross[AlmondRx](ScalaVersions.scala212, ScalaVersions.scala213) object `toree-hooks` extends Cross[ToreeHooks](ScalaVersions.binaries: _*) @@ -568,6 +591,7 @@ class KernelLocalRepo(val testScalaVersion: String) extends LocalRepo { scala.`scala-interpreter`(testScalaVersion), scala.`toree-hooks`(ScalaVersions.binary(testScalaVersion)), scala.`coursier-logger`(ScalaVersions.binary(testScalaVersion)), + scala.`shared-directives`(ScalaVersions.binary(testScalaVersion)), scala.launcher, shared.kernel(ScalaVersions.binary(ScalaVersions.scala3Latest)), shared.interpreter(ScalaVersions.binary(ScalaVersions.scala3Latest)), 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 d6da46194..288659e92 100644 --- a/modules/scala/integration/src/main/scala/almond/integration/KernelTestsDefinitions.scala +++ b/modules/scala/integration/src/main/scala/almond/integration/KernelTestsDefinitions.scala @@ -117,4 +117,25 @@ abstract class KernelTestsDefinitions extends AlmondFunSuite { } } + test("add dependency") { + kernelLauncher.withKernel { implicit runner => + implicit val sessionId: SessionId = SessionId() + almond.integration.Tests.addDependency() + } + } + + test("add repository") { + kernelLauncher.withKernel { implicit runner => + implicit val sessionId: SessionId = SessionId() + almond.integration.Tests.addRepository() + } + } + + test("add scalac option") { + kernelLauncher.withKernel { implicit runner => + implicit val sessionId: SessionId = SessionId() + almond.integration.Tests.addScalacOption(kernelLauncher.defaultScalaVersion) + } + } + } 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 fc3a5514c..8171d02b4 100644 --- a/modules/scala/integration/src/test/scala/almond/integration/KernelTestsTwoStepStartup213.scala +++ b/modules/scala/integration/src/test/scala/almond/integration/KernelTestsTwoStepStartup213.scala @@ -1,8 +1,185 @@ package almond.integration +import almond.testkit.Dsl._ + class KernelTestsTwoStepStartup213 extends KernelTestsDefinitions { lazy val kernelLauncher = new KernelLauncher(KernelLauncher.LauncherType.Jvm, KernelLauncher.testScala213Version) + test("mixed directives") { + kernelLauncher.withKernel { implicit runner => + implicit val sessionId: SessionId = SessionId() + runner.withSession() { implicit session => + + execute( + """//> using scala "2.13.11" + |2 + |""".stripMargin, + "res1: Int = 2" + ) + execute( + """//> using option "-deprecation" "-Xfatal-warnings" + | + |@deprecated + |def foo() = 2 + | + |foo() + |""".stripMargin, + expectError = true, + stderr = + """cell2.sc:4: method foo in class Helper is deprecated + |val res2_1 = foo() + | ^ + |No warnings can be incurred under -Werror. + |Compilation Failed""".stripMargin, + errors = Seq( + ("", "Compilation Failed", List("Compilation Failed")) + ) + ) + } + } + } + + test("mixed directives in first cell") { + kernelLauncher.withKernel { implicit runner => + implicit val sessionId: SessionId = SessionId() + runner.withSession() { implicit session => + + execute( + """//> using scala "2.13.11" + |//> using option "-deprecation" "-Xfatal-warnings" + |2 + |""".stripMargin, + "res1: Int = 2" + ) + execute( + """@deprecated + |def foo() = 2 + | + |foo() + |""".stripMargin, + expectError = true, + stderr = + """cell2.sc:4: method foo in class Helper is deprecated + |val res2_1 = foo() + | ^ + |No warnings can be incurred under -Werror. + |Compilation Failed""".stripMargin, + errors = Seq( + ("", "Compilation Failed", List("Compilation Failed")) + ) + ) + } + } + } + + test("mixed directives single cell") { + kernelLauncher.withKernel { implicit runner => + implicit val sessionId: SessionId = SessionId() + runner.withSession() { implicit session => + + execute( + """//> using scala "2.13.11" + |//> using option "-deprecation" "-Xfatal-warnings" + | + |@deprecated + |def foo() = 2 + | + |foo() + |""".stripMargin, + expectError = true, + stderr = + """cell1.sc:4: method foo in class Helper is deprecated + |val res1_1 = foo() + | ^ + |No warnings can be incurred under -Werror. + |Compilation Failed""".stripMargin, + errors = Seq( + ("", "Compilation Failed", List("Compilation Failed")) + ) + ) + } + } + } + + test("mixed directives several kernel options") { + kernelLauncher.withKernel { implicit runner => + implicit val sessionId: SessionId = SessionId() + runner.withSession() { implicit session => + + execute( + """//> using scala "2.12.18" + |//> using option "-Xfatal-warnings" + |""".stripMargin, + "" + ) + + execute( + """//> using option "-deprecation" + | + |@deprecated + |def foo() = 2 + | + |foo() + |""".stripMargin, + expectError = true, + stderr = + """cell2.sc:4: method foo in class Helper is deprecated + |val res2_1 = foo() + | ^ + |No warnings can be incurred under -Xfatal-warnings. + |Compilation Failed""".stripMargin, + errors = Seq( + ("", "Compilation Failed", List("Compilation Failed")) + ) + ) + } + } + } + + test("late launcher directives") { + kernelLauncher.withKernel { implicit runner => + implicit val sessionId: SessionId = SessionId() + runner.withSession() { implicit session => + + execute( + """//> using scala "2.12.18" + |//> using option "-Xfatal-warnings" + |""".stripMargin, + "" + ) + + execute( + """//> using option "-deprecation" + | + |@deprecated + |def foo() = 2 + | + |foo() + |""".stripMargin, + expectError = true, + stderr = + """cell2.sc:4: method foo in class Helper is deprecated + |val res2_1 = foo() + | ^ + |No warnings can be incurred under -Xfatal-warnings. + |Compilation Failed""".stripMargin, + errors = Seq( + ("", "Compilation Failed", List("Compilation Failed")) + ) + ) + + execute( + """//> using scala "2.13.11"""", + "", + stderr = + """Warning: ignoring 1 directive(s) that can only be used prior to any code: + | //> using scala + |""".stripMargin + ) + } + } + } + } 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 103500dfd..e9f3b5157 100644 --- a/modules/scala/launcher/src/main/scala/almond/launcher/Launcher.scala +++ b/modules/scala/launcher/src/main/scala/almond/launcher/Launcher.scala @@ -3,10 +3,12 @@ package almond.launcher import almond.channels.{Channel, Connection, Message => RawMessage} import almond.channels.zeromq.ZeromqThreads import almond.cslogger.NotebookCacheLogger +import almond.directives.KernelOptions import almond.interpreter.ExecuteResult import almond.interpreter.api.{DisplayData, OutputHandler} import almond.kernel.install.Install import almond.kernel.{Kernel, KernelThreads, MessageFile} +import almond.launcher.directives.LauncherParameters import almond.logger.{Level, LoggerContext} import almond.protocol.{Execute, RawJson} import almond.util.ThreadUtil.singleThreadedExecutionContext @@ -14,6 +16,7 @@ import caseapp.core.RemainingArgs import caseapp.core.app.CaseApp import cats.effect.IO import cats.effect.unsafe.IORuntime +import com.github.plokhotnyuk.jsoniter_scala.core._ import coursier.launcher.{BootstrapGenerator, ClassLoaderContent, ClassPathEntry, Parameters} import dependency.ScalaParameters @@ -33,6 +36,7 @@ object Launcher extends CaseApp[LauncherOptions] { options: LauncherOptions, noExecuteInputFor: Seq[String], params0: LauncherParameters, + kernelOptions: KernelOptions, outputHandler: OutputHandler, logCtx: LoggerContext ): (os.proc, String, Option[String]) = { @@ -146,9 +150,18 @@ object Launcher extends CaseApp[LauncherOptions] { Seq[os.Shellable]("--leftover-messages", msgFile) } val noExecuteInputArgs = noExecuteInputFor.flatMap { id => - Seq("--no-execute-input-for", id) + Seq("--no-execute-input-for", id, "--ignore-launcher-directives-in", id) } + val optionsArgs = + if (kernelOptions.isEmpty) Nil + else { + val asJson = KernelOptions.AsJson(kernelOptions) + val bytes = writeToArray(asJson)(KernelOptions.AsJson.codec) + val optionsFile = os.temp(bytes, prefix = "almond-options-", suffix = ".json") + Seq[os.Shellable]("--kernel-options", optionsFile) + } + val jvmIdOpt = params0.jvm.filter(_.trim.nonEmpty) val javaCommand = jvmIdOpt match { case Some(jvmId) => @@ -183,6 +196,7 @@ object Launcher extends CaseApp[LauncherOptions] { currentCellCount, msgFileArgs, noExecuteInputArgs, + optionsArgs, options.kernelOptions ) @@ -359,6 +373,7 @@ object Launcher extends CaseApp[LauncherOptions] { options, firstMessageIdOpt.toSeq, interpreter.params, + interpreter.kernelOptions, outputHandlerOpt.getOrElse(OutputHandler.NopOutputHandler), logCtx ) diff --git a/modules/scala/launcher/src/main/scala/almond/launcher/LauncherInterpreter.scala b/modules/scala/launcher/src/main/scala/almond/launcher/LauncherInterpreter.scala index 2d48bfab7..d373e2e6a 100644 --- a/modules/scala/launcher/src/main/scala/almond/launcher/LauncherInterpreter.scala +++ b/modules/scala/launcher/src/main/scala/almond/launcher/LauncherInterpreter.scala @@ -1,9 +1,12 @@ package almond.launcher +import almond.directives.{HasKernelOptions, KernelOptions} +import almond.directives.HasKernelOptions.ops._ import almond.interpreter.api.OutputHandler import almond.interpreter.{ExecuteResult, Interpreter} import almond.interpreter.api.DisplayData import almond.interpreter.input.InputManager +import almond.launcher.directives.{HasLauncherParameters, LauncherParameters} import almond.protocol.KernelInfo import java.io.File @@ -37,7 +40,8 @@ class LauncherInterpreter( help_links = None // Some(params.extraLinks.toList).filter(_.nonEmpty) ) - var params = LauncherParameters() + var kernelOptions = KernelOptions() + var params = LauncherParameters() def execute( code: String, @@ -47,21 +51,42 @@ class LauncherInterpreter( ): ExecuteResult = { val path = Left(s"cell$lineCount0.sc") val scopePath = ScopePath(Left("."), os.sub) - val maybeUpdate = LauncherInterpreter.handlers0.parse(code, path, scopePath) - .map { res => - res - .flatMap(_.global.map(_.launcherParameters).toSeq) - .foldLeft(LauncherParameters())(_ + _) - } - maybeUpdate match { + val maybeParamsUpdate = + LauncherInterpreter.launcherParametersHandlers.parse(code, path, scopePath) + .map { res => + res + .flatMap(_.global.map(_.launcherParameters).toSeq) + .foldLeft(LauncherParameters())(_ + _) + } + val maybeKernelOptionsUpdate = + LauncherInterpreter.kernelOptionsHandlers.parse(code, path, scopePath) + .flatMap { res => + res + .flatMap(_.global.map(_.kernelOptions).toSeq) + .sequence + .map(_.foldLeft(KernelOptions())(_ + _)) + .left.map(CompositeDirectiveException(_)) + } + val maybeUpdates = (maybeParamsUpdate, maybeKernelOptionsUpdate) match { + case (Left(err1), Left(err2)) => + Left(CompositeDirectiveException(Seq(err1, err2))) + case (Left(err1), Right(_)) => + Left(err1) + case (Right(_), Left(err2)) => + Left(err2) + case (Right(paramsUpdate), Right(kernelUpdate)) => + Right((paramsUpdate, kernelUpdate)) + } + maybeUpdates match { case Left(ex) => LauncherInterpreter.error( LauncherInterpreter.Colors.default, Some(ex), "Error while processing using directives" ) - case Right(paramsUpdate) => + case Right((paramsUpdate, kernelOptionsUpdate)) => params = params + paramsUpdate + kernelOptions = kernelOptions + kernelOptionsUpdate if (ScalaParser.hasActualCode(code)) // handing over execution to the actual kernel ExecuteResult.Close @@ -80,13 +105,12 @@ class LauncherInterpreter( object LauncherInterpreter { - private val handlers = Seq[DirectiveHandler[directives.HasLauncherParameters]]( - directives.JavaOptions.handler, - directives.Jvm.handler, - directives.ScalaVersion.handler - ) - - private val handlers0 = DirectiveHandlers(handlers) + private val launcherParametersHandlers = + LauncherParameters.handlers ++ + HasKernelOptions.handlers.map(_ => HasLauncherParameters.Ignore) + private val kernelOptionsHandlers = + HasKernelOptions.handlers ++ + LauncherParameters.handlers.map(_ => HasKernelOptions.Ignore) private def error(colors: Colors, exOpt: Option[Throwable], msg: String) = ExecuteResult.Error.error(colors.error, colors.literal, exOpt, msg) diff --git a/modules/scala/launcher/src/main/scala/almond/launcher/LauncherOutputHandler.scala b/modules/scala/launcher/src/main/scala/almond/launcher/LauncherOutputHandler.scala index 5977c9cad..c9a1a595a 100644 --- a/modules/scala/launcher/src/main/scala/almond/launcher/LauncherOutputHandler.scala +++ b/modules/scala/launcher/src/main/scala/almond/launcher/LauncherOutputHandler.scala @@ -102,4 +102,6 @@ class LauncherOutputHandler( def updateDisplay(displayData: almond.interpreter.api.DisplayData): Unit = queue.add(UpdateDisplay(displayData)) def canOutput(): Boolean = true + + def messageIdOpt: Option[String] = None } diff --git a/modules/scala/launcher/src/main/scala/almond/launcher/directives/HasLauncherParameters.scala b/modules/scala/launcher/src/main/scala/almond/launcher/directives/HasLauncherParameters.scala deleted file mode 100644 index f04dc737d..000000000 --- a/modules/scala/launcher/src/main/scala/almond/launcher/directives/HasLauncherParameters.scala +++ /dev/null @@ -1,7 +0,0 @@ -package almond.launcher.directives - -import almond.launcher.LauncherParameters - -trait HasLauncherParameters { - def launcherParameters: LauncherParameters -} diff --git a/modules/scala/scala-interpreter/src/main/scala-2/almond/internals/ConfigureCompiler.scala b/modules/scala/scala-interpreter/src/main/scala-2/almond/internals/ConfigureCompiler.scala new file mode 100644 index 000000000..1efb780cf --- /dev/null +++ b/modules/scala/scala-interpreter/src/main/scala-2/almond/internals/ConfigureCompiler.scala @@ -0,0 +1,13 @@ +package almond.internals + +import ammonite.compiler.CompilerExtensions._ +import ammonite.interp.api.InterpAPI + +object ConfigureCompiler { + def addOptions(interpApi: InterpAPI)(options: Seq[String]): Unit = + interpApi.preConfigureCompiler { settings => + val (success, _) = settings.processArguments(options.toList, true) + if (!success) + throw new Exception(s"Failed to add Scalac options ${options.mkString(" ")}") + } +} diff --git a/modules/scala/scala-interpreter/src/main/scala-3/almond/internals/ConfigureCompiler.scala b/modules/scala/scala-interpreter/src/main/scala-3/almond/internals/ConfigureCompiler.scala new file mode 100644 index 000000000..3755f8c2e --- /dev/null +++ b/modules/scala/scala-interpreter/src/main/scala-3/almond/internals/ConfigureCompiler.scala @@ -0,0 +1,15 @@ +package almond.internals + +import ammonite.compiler.CompilerExtensions._ +import ammonite.compiler.CompilerLifecycleManager +import ammonite.interp.api.InterpAPI +import dotty.tools.dotc.ScalacCommand + +object ConfigureCompiler { + def addOptions(interpApi: InterpAPI)(options: Seq[String]): Unit = + interpApi.preConfigureCompiler { ctx => + val summary = + ScalacCommand.distill(options.toArray, ctx.settings)(ctx.settingsState)(using ctx) + ctx.setSettings(summary.sstate) + } +} 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 118726af3..49024a18b 100644 --- a/modules/scala/scala-interpreter/src/main/scala/almond/Execute.scala +++ b/modules/scala/scala-interpreter/src/main/scala/almond/Execute.scala @@ -5,6 +5,8 @@ import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets.UTF_8 import almond.api.JupyterApi +import almond.directives.{HasKernelOptions, KernelOptions} +import almond.directives.HasKernelOptions.ops._ import almond.internals.{ Capture, FunctionInputStream, @@ -15,14 +17,20 @@ import almond.internals.{ import almond.interpreter.ExecuteResult import almond.interpreter.api.{CommHandler, DisplayData, OutputHandler} import almond.interpreter.input.InputManager +import almond.launcher.directives.LauncherParameters import almond.logger.LoggerContext import ammonite.compiler.Parsers import ammonite.repl.api.History import ammonite.repl.{Repl, Signaller} import ammonite.runtime.Storage import ammonite.util.{Colors, Ex, Printer, Ref, Res} +import coursierapi.{IvyRepository, MavenRepository} +import dependency.ScalaParameters +import dependency.api.ops._ import fastparse.Parsed +import scala.cli.directivehandler._ +import scala.cli.directivehandler.EitherSequence._ import scala.collection.mutable import scala.concurrent.{Await, ExecutionContext} import scala.concurrent.duration.Duration @@ -40,9 +48,13 @@ final class Execute( silent: Ref[Boolean], useThreadInterrupt: Boolean, initialCellCount: Int, - enableExitHack: Boolean + enableExitHack: Boolean, + ignoreLauncherDirectivesIn: Set[String] ) { + private val handlers = HasKernelOptions.handlers ++ + LauncherParameters.handlers.mapDirectives(_.ignoredDirective) + private val log = logCtx(getClass) private var currentInputManagerOpt0 = Option.empty[InputManager] @@ -113,6 +125,41 @@ final class Execute( s => currentPublishOpt0.fold(Console.err.println(s))(_.stdout(s + System.lineSeparator())) ) + private def useOptions( + ammInterp: ammonite.interp.Interpreter, + options: KernelOptions + ): Either[String, Unit] = { + + for (input <- options.extraRepositories) { + val repo = + if (input.startsWith("ivy:")) + IvyRepository.of(input.drop("ivy:".length)) + else + MavenRepository.of(input) + ammInterp.repositories.update(ammInterp.repositories() :+ repo) + } + + almond.internals.ConfigureCompiler.addOptions(ammInterp.interpApi)( + options.scalacOptions.toSeq.map(_.value.value) + ) + + val params = ScalaParameters(ammInterp.scalaVersion) + val compatParams = ScalaParameters(scala.util.Properties.versionNumberString) + val deps = options.dependencies.map { dep => + val params0 = + if (dep.userParams.get("compat").nonEmpty) compatParams + else params + dep.applyParams(params0).toCs + } + val loadDepsRes = + if (deps.isEmpty) Right(Nil) + else ammInterp.loadIvy(deps: _*) + loadDepsRes.map { loaded => + ammInterp.headFrame.addClasspath(loaded.map(_.toURI.toURL)) + () + } + } + def history: History = history0 @@ -233,6 +280,19 @@ final class Execute( def lastExceptionOpt: Option[Throwable] = lastExceptionOpt0 + private def incrementLine(storeHistory: Boolean): Unit = + if (storeHistory) + currentLine0 += 1 + else + currentNoHistoryLine0 += 1 + + def loadOptions(ammInterp: ammonite.interp.Interpreter, options: KernelOptions): Unit = + useOptions(ammInterp, options) match { + case Left(err) => + log.warn(s"Error loading initial kernel options: $err") + case Right(()) => + } + private def ammResult( ammInterp: ammonite.interp.Interpreter, code: String, @@ -273,11 +333,7 @@ final class Execute( stmts, (if (storeHistory) currentLine0 else currentNoHistoryLine0) + 1, silent = silent(), - incrementLine = - if (storeHistory) - () => currentLine0 += 1 - else - () => currentNoHistoryLine0 += 1 + incrementLine = () => incrementLine(storeHistory) ) log.debug(s"Handling output of '$code0'") @@ -403,42 +459,95 @@ final class Execute( ExecuteResult.Success() case Success(Right(finalCode)) => - ammResult(ammInterp, finalCode, inputManager, outputHandler, storeHistory) match { - case Res.Success((_, data)) => - ExecuteResult.Success(data) - case Res.Failure(msg) => - interruptedStackTraceOpt0 match { - case None => - val err = Execute.error(colors0(), None, msg) - outputHandler.foreach(_.stderr(err.message)) // necessary? - err - case Some(st) => - val cutoff = Set("$main", "evaluatorRunPrinter") - - ExecuteResult.Error( - "Interrupted!", - "", - List("Interrupted!") ++ st - .takeWhile(x => !cutoff(x.getMethodName)) - .map(ExecuteResult.Error.highlightFrame( - _, - fansi.Attr.Reset, - colors0().literal() - )) - .map(_.render) - .toList - ) - } + val path = Left(s"cell$currentLine0.sc") + val scopePath = ScopePath(Left("."), os.sub) + handlers.parse(finalCode, path, scopePath) match { + case Left(err) => + log.error(s"exception while processing directives (${err.getMessage})", err) + Execute.error(colors0(), Some(err), err.getMessage) + case Right(res) => + val maybeOptions = res + .flatMap(_.global.map(_.kernelOptions).toSeq) + .sequence + .map(_.foldLeft(KernelOptions())(_ + _)) + maybeOptions match { + case Left(err) => + // FIXME Use positions in the exception to report errors as diagnostics + // FIXME This discards all errors but the first + Execute.error(colors0(), Some(err.head), "") + case Right(options) => + val optionsRes = useOptions(ammInterp, options) + + if ( + options.ignoredDirectives.nonEmpty && + outputHandler + .flatMap(_.messageIdOpt) + .forall(id => !ignoreLauncherDirectivesIn.contains(id)) + ) { + def printErr(s: String): Unit = + outputHandler match { + case Some(h) => h.stderr(s + System.lineSeparator()) + case None => System.err.println(s) + } + printErr( + s"Warning: ignoring ${options.ignoredDirectives.length} directive(s) that can only be used prior to any code:" + ) + for (dir <- options.ignoredDirectives.map(_.directive)) + printErr(s" //> using ${dir.directive.key}") + } - case Res.Exception(ex, msg) => - log.error(s"exception in user code (${ex.getMessage})", ex) - Execute.error(colors0(), Some(ex), msg) + optionsRes match { + case Left(failureMsg) => + // kind of meh that we have to build a new Exception here + Execute.error(colors0(), Some(new Exception(failureMsg)), "") + case Right(()) => + ammResult( + ammInterp, + finalCode, + inputManager, + outputHandler, + storeHistory + ) match { + case Res.Success((_, data)) => + ExecuteResult.Success(data) + case Res.Failure(msg) => + interruptedStackTraceOpt0 match { + case None => + val err = Execute.error(colors0(), None, msg) + outputHandler.foreach(_.stderr(err.message)) // necessary? + err + case Some(st) => + val cutoff = Set("$main", "evaluatorRunPrinter") + + ExecuteResult.Error( + "Interrupted!", + "", + List("Interrupted!") ++ st + .takeWhile(x => !cutoff(x.getMethodName)) + .map(ExecuteResult.Error.highlightFrame( + _, + fansi.Attr.Reset, + colors0().literal() + )) + .map(_.render) + .toList + ) + } - case Res.Skip => - ExecuteResult.Success() + case Res.Exception(ex, msg) => + log.error(s"exception in user code (${ex.getMessage})", ex) + Execute.error(colors0(), Some(ex), msg) - case Res.Exit(_) => - ExecuteResult.Exit + case Res.Skip => + if (!options.isEmpty) + incrementLine(storeHistory) + ExecuteResult.Success() + + case Res.Exit(_) => + ExecuteResult.Exit + } + } + } } } } 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 0332d4d06..c9f8f0f66 100644 --- a/modules/scala/scala-interpreter/src/main/scala/almond/ScalaInterpreter.scala +++ b/modules/scala/scala-interpreter/src/main/scala/almond/ScalaInterpreter.scala @@ -73,7 +73,8 @@ final class ScalaInterpreter( silent0, params.useThreadInterrupt, params.initialCellCount, - enableExitHack = params.compileOnly + enableExitHack = params.compileOnly, + ignoreLauncherDirectivesIn = params.ignoreLauncherDirectivesIn ) val sessApi = new SessionApiImpl(frames0) @@ -109,7 +110,7 @@ final class ScalaInterpreter( for (ec <- params.updateBackgroundVariablesEcOpt) UpdatableFuture.setup(replApi, jupyterApi, ec) - AmmInterpreter( + val interp = AmmInterpreter( execute0, storage, replApi, @@ -134,6 +135,10 @@ final class ScalaInterpreter( compileOnly = params.compileOnly, addToreeApiCompatibilityImport = params.toreeApiCompatibility ) + + execute0.loadOptions(interp, params.upfrontKernelOptions) + + interp } if (!params.lazyInit) 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 9e43c6e85..6c2e65ea2 100644 --- a/modules/scala/scala-interpreter/src/main/scala/almond/ScalaInterpreterParams.scala +++ b/modules/scala/scala-interpreter/src/main/scala/almond/ScalaInterpreterParams.scala @@ -2,6 +2,7 @@ package almond import java.nio.file.Path +import almond.directives.KernelOptions import almond.protocol.KernelInfo import ammonite.compiler.iface.CodeWrapper import ammonite.compiler.CodeClassWrapper @@ -41,5 +42,7 @@ final case class ScalaInterpreterParams( toreeApiCompatibility: Boolean = false, compileOnly: Boolean = false, extraClassPath: List[os.Path] = Nil, - initialCellCount: Int = 0 + initialCellCount: Int = 0, + upfrontKernelOptions: KernelOptions = KernelOptions(), + ignoreLauncherDirectivesIn: Set[String] = Set.empty ) diff --git a/modules/scala/scala-interpreter/src/test/scala/almond/MockOutputHandler.scala b/modules/scala/scala-interpreter/src/test/scala/almond/MockOutputHandler.scala index 1ef1409ad..6533ef249 100644 --- a/modules/scala/scala-interpreter/src/test/scala/almond/MockOutputHandler.scala +++ b/modules/scala/scala-interpreter/src/test/scala/almond/MockOutputHandler.scala @@ -25,4 +25,5 @@ class MockOutputHandler extends OutputHandler { def updateDisplay(displayData: almond.interpreter.api.DisplayData): Unit = () def canOutput(): Boolean = false + def messageIdOpt: Option[String] = None } 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 7c5872742..0900e3513 100644 --- a/modules/scala/scala-kernel/src/main/scala/almond/Options.scala +++ b/modules/scala/scala-kernel/src/main/scala/almond/Options.scala @@ -5,6 +5,7 @@ import java.util.regex.Pattern import almond.api.Properties import almond.channels.{Channel, Message => RawMessage} +import almond.directives.KernelOptions import almond.kernel.MessageFile import almond.kernel.install.{Options => InstallOptions} import almond.protocol.KernelInfo @@ -113,7 +114,15 @@ final case class Options( @HelpMessage("Do not send execute_input message for incoming messages with the passed ids") @Hidden - noExecuteInputFor: List[String] = Nil + noExecuteInputFor: List[String] = Nil, + + @HelpMessage("Path of JSON file with using directives kernel options") + @Hidden + kernelOptions: Option[String] = None, + + @HelpMessage("Do warn users about launcher directives for incoming messages with the passed ids") + @Hidden + ignoreLauncherDirectivesIn: List[String] = Nil ) { // format: on @@ -277,6 +286,13 @@ final case class Options( msgFile.parsedMessages } + def readKernelOptions(): Option[KernelOptions.AsJson] = + kernelOptions.map { strPath => + val path = os.Path(strPath, os.pwd) + val bytes = os.read.bytes(path) + readFromArray(bytes)(KernelOptions.AsJson.codec) + } + } object Options { 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 6c7b43b9f..d904f6e32 100644 --- a/modules/scala/scala-kernel/src/main/scala/almond/ScalaKernel.scala +++ b/modules/scala/scala-kernel/src/main/scala/almond/ScalaKernel.scala @@ -4,6 +4,7 @@ import java.io.{File, FileOutputStream, PrintStream} import almond.api.JupyterApi import almond.channels.zeromq.ZeromqThreads +import almond.directives.KernelOptions import almond.interpreter.messagehandlers.MessageHandler import almond.kernel.{Kernel, KernelThreads} import almond.kernel.install.Install @@ -130,6 +131,21 @@ object ScalaKernel extends CaseApp[Options] { log.debug("Creating interpreter") + val kernelOptionsFromJson = options.readKernelOptions() match { + case Some(asJson) => + asJson.toKernelOptions match { + case Left(errors) => + log.warn( + s"Got errors when trying to read options from ${options.kernelOptions.getOrElse("???")}: ${errors.mkString(", ")}" + ) + KernelOptions() + case Right(options) => + options + } + case None => + KernelOptions() + } + val interpreter = new ScalaInterpreter( params = ScalaInterpreterParams( updateBackgroundVariablesEcOpt = Some(updateBackgroundVariablesEc), @@ -174,7 +190,9 @@ object ScalaKernel extends CaseApp[Options] { ClassPathUtil.classPath(input) .map(os.Path(_, os.pwd)) }, - initialCellCount = options.initialCellCount.getOrElse(0) + initialCellCount = options.initialCellCount.getOrElse(0), + upfrontKernelOptions = kernelOptionsFromJson, + ignoreLauncherDirectivesIn = options.ignoreLauncherDirectivesIn.toSet ), logCtx = logCtx ) diff --git a/modules/scala/shared-directives/src/main/scala/almond/directives/AddDependency.scala b/modules/scala/shared-directives/src/main/scala/almond/directives/AddDependency.scala new file mode 100644 index 000000000..eda1936cb --- /dev/null +++ b/modules/scala/shared-directives/src/main/scala/almond/directives/AddDependency.scala @@ -0,0 +1,38 @@ +package almond.directives + +import scala.cli.directivehandler._ +import scala.cli.directivehandler.EitherSequence._ + +@DirectiveGroupName("Dependency options") +@DirectiveExamples("//> using dep \"com.lihaoyi::os-lib:0.9.1\"") +@DirectiveUsage( + "//> using dep _deps_", + "`//> using dep `_deps_" +) +@DirectiveDescription("Add dependencies") +final case class AddDependency( + @DirectiveName("lib") + @DirectiveName("libs") + @DirectiveName("dep") + @DirectiveName("deps") + @DirectiveName("dependencies") + dependency: List[Positioned[String]] = Nil +) extends HasKernelOptions { + def kernelOptions = { + val maybeDeps = dependency + .map { posInput => + _root_.dependency.parser.DependencyParser.parse(posInput.value) + .left.map(err => new MalformedDependencyException(err, posInput.positions)) + } + .sequence + .left.map(CompositeDirectiveException(_)) + + maybeDeps.map { deps => + KernelOptions(dependencies = deps) + } + } +} + +object AddDependency { + val handler: DirectiveHandler[AddDependency] = DirectiveHandler.deriver[AddDependency].derive +} diff --git a/modules/scala/shared-directives/src/main/scala/almond/directives/HasKernelOptions.scala b/modules/scala/shared-directives/src/main/scala/almond/directives/HasKernelOptions.scala new file mode 100644 index 000000000..f1b5339d0 --- /dev/null +++ b/modules/scala/shared-directives/src/main/scala/almond/directives/HasKernelOptions.scala @@ -0,0 +1,41 @@ +package almond.directives + +import scala.cli.directivehandler.{ + DirectiveException, + DirectiveHandler, + DirectiveHandlers, + IgnoredDirective +} + +trait HasKernelOptions { + def kernelOptions: Either[DirectiveException, KernelOptions] +} + +object HasKernelOptions { + + case object Ignore extends HasKernelOptions { + def kernelOptions: Either[DirectiveException, KernelOptions] = + Right(KernelOptions()) + } + + final case class IgnoredDirectives(ignored: Seq[IgnoredDirective]) extends HasKernelOptions { + def kernelOptions: Either[DirectiveException, KernelOptions] = + Right(KernelOptions(ignoredDirectives = ignored)) + } + + object ops { + implicit class HasKernelOptionsDirectiveHandlerOps[T]( + private val handler: DirectiveHandler[T] + ) { + def ignoredDirective: DirectiveHandler[HasKernelOptions] = + handler.ignore.map(ignored => IgnoredDirectives(Seq(ignored))) + } + } + + val handlers = DirectiveHandlers(Seq[DirectiveHandler[HasKernelOptions]]( + AddDependency.handler, + Repository.handler, + ScalacOptions.handler + )) + +} diff --git a/modules/scala/shared-directives/src/main/scala/almond/directives/KernelOptions.scala b/modules/scala/shared-directives/src/main/scala/almond/directives/KernelOptions.scala new file mode 100644 index 000000000..ac5092d3c --- /dev/null +++ b/modules/scala/shared-directives/src/main/scala/almond/directives/KernelOptions.scala @@ -0,0 +1,75 @@ +package almond.directives + +import com.github.plokhotnyuk.jsoniter_scala.core._ +import com.github.plokhotnyuk.jsoniter_scala.macros._ +import dependency.parser.DependencyParser + +import scala.cli.directivehandler._ +import scala.cli.directivehandler.EitherSequence._ + +// other directives that could be imported from Scala CLI: +// - exclude +// - objectWrapper +// - plugin +// - python +// - resources +// - toolkit +final case class KernelOptions( + dependencies: Seq[dependency.AnyDependency] = Nil, + scalacOptions: ShadowingSeq[Positioned[ScalacOpt]] = ShadowingSeq.empty, + extraRepositories: Seq[String] = Nil, + ignoredDirectives: Seq[IgnoredDirective] = Nil +) { + def isEmpty: Boolean = + this == KernelOptions() + def +(other: KernelOptions): KernelOptions = + copy( + dependencies = dependencies ++ other.dependencies, + scalacOptions = scalacOptions ++ other.scalacOptions.toSeq, + extraRepositories = extraRepositories ++ other.extraRepositories, + ignoredDirectives = ignoredDirectives ++ other.ignoredDirectives + ) +} + +object KernelOptions { + + final case class AsJson( + dependencies: Seq[String] = Nil, + scalacOptions: Seq[String] = Nil, + extraRepositories: Seq[String] = Nil + ) { + def toKernelOptions: Either[::[String], KernelOptions] = { + val maybeDependencies = dependencies + .map { dep => + DependencyParser.parse(dep) + } + .sequence + .left.map(errors => ::(errors.head, errors.tail.toList)) + maybeDependencies match { + case Right(dependencies0) => + Right( + KernelOptions( + dependencies = dependencies0, + scalacOptions = + ShadowingSeq.from(scalacOptions.map(opt => Positioned.none(ScalacOpt(opt)))), + extraRepositories = extraRepositories + ) + ) + case Left(depErrors) => + Left(depErrors) + } + } + } + + object AsJson { + val codec: JsonValueCodec[AsJson] = JsonCodecMaker.make + + def apply(options: KernelOptions): AsJson = + AsJson( + dependencies = options.dependencies.map(_.render), + scalacOptions = options.scalacOptions.toSeq.map(_.value.value), + extraRepositories = options.extraRepositories + ) + } + +} diff --git a/modules/scala/shared-directives/src/main/scala/almond/directives/MalformedDependencyException.scala b/modules/scala/shared-directives/src/main/scala/almond/directives/MalformedDependencyException.scala new file mode 100644 index 000000000..a5d69bbce --- /dev/null +++ b/modules/scala/shared-directives/src/main/scala/almond/directives/MalformedDependencyException.scala @@ -0,0 +1,6 @@ +package almond.directives + +import scala.cli.directivehandler.{DirectiveException, Position} + +final class MalformedDependencyException(message: String, positions: Seq[Position]) + extends DirectiveException(message, positions) diff --git a/modules/scala/shared-directives/src/main/scala/almond/directives/Repository.scala b/modules/scala/shared-directives/src/main/scala/almond/directives/Repository.scala new file mode 100644 index 000000000..b409f79ea --- /dev/null +++ b/modules/scala/shared-directives/src/main/scala/almond/directives/Repository.scala @@ -0,0 +1,39 @@ +package almond.directives + +import scala.cli.directivehandler._ +import scala.cli.directivehandler.EitherSequence._ + +@DirectiveGroupName("Repository") +@DirectiveExamples("//> using repository jitpack") +@DirectiveExamples("//> using repository sonatype:snapshots") +@DirectiveExamples("//> using repository m2Local") +@DirectiveExamples( + "//> using repository https://maven-central.storage-download.googleapis.com/maven2" +) +@DirectiveUsage( + "//> using repository _repository_", + "`//> using repository `_repository_" +) +@DirectiveDescription(Repository.usageMsg) +// format: off +final case class Repository( + @DirectiveName("repository") + repositories: List[String] = Nil +) extends HasKernelOptions { + // format: on + def kernelOptions = + Right( + KernelOptions( + extraRepositories = repositories + ) + ) +} + +object Repository { + val handler: DirectiveHandler[Repository] = DirectiveHandler.deriver[Repository].derive + + val usageMsg = + """Add repositories for dependency resolution. + | + |Accepts predefined repositories supported by Coursier (like `sonatype:snapshots` or `m2Local`) or a URL of the root of Maven repository""".stripMargin +} diff --git a/modules/scala/shared-directives/src/main/scala/almond/directives/ScalacOptions.scala b/modules/scala/shared-directives/src/main/scala/almond/directives/ScalacOptions.scala new file mode 100644 index 000000000..22d9b0244 --- /dev/null +++ b/modules/scala/shared-directives/src/main/scala/almond/directives/ScalacOptions.scala @@ -0,0 +1,30 @@ +package almond.directives + +import scala.cli.directivehandler._ + +@DirectiveGroupName("Compiler options") +@DirectiveExamples("//> using option -Xasync") +@DirectiveExamples("//> using test.option -Xasync") +@DirectiveExamples("//> using options -Xasync, -Xfatal-warnings") +@DirectiveUsage( + "using option _option_ | using options _option1_ _option2_ …", + """`//> using option `_option_ + | + |`//> using options `_option1_, _option2_ …""".stripMargin +) +@DirectiveDescription("Add Scala compiler options") +final case class ScalacOptions( + @DirectiveName("option") + options: List[Positioned[String]] = Nil +) extends HasKernelOptions { + def kernelOptions = + Right( + KernelOptions( + scalacOptions = ShadowingSeq.from(options.map(_.map(ScalacOpt(_)))) + ) + ) +} + +object ScalacOptions { + val handler: DirectiveHandler[ScalacOptions] = DirectiveHandler.deriver[ScalacOptions].derive +} diff --git a/modules/scala/shared-directives/src/main/scala/almond/launcher/directives/HasLauncherParameters.scala b/modules/scala/shared-directives/src/main/scala/almond/launcher/directives/HasLauncherParameters.scala new file mode 100644 index 000000000..073ed9b9f --- /dev/null +++ b/modules/scala/shared-directives/src/main/scala/almond/launcher/directives/HasLauncherParameters.scala @@ -0,0 +1,14 @@ +package almond.launcher.directives + +trait HasLauncherParameters { + def launcherParameters: LauncherParameters +} + +object HasLauncherParameters { + + case object Ignore extends HasLauncherParameters { + def launcherParameters: LauncherParameters = + LauncherParameters() + } + +} diff --git a/modules/scala/launcher/src/main/scala/almond/launcher/directives/JavaOptions.scala b/modules/scala/shared-directives/src/main/scala/almond/launcher/directives/JavaOptions.scala similarity index 93% rename from modules/scala/launcher/src/main/scala/almond/launcher/directives/JavaOptions.scala rename to modules/scala/shared-directives/src/main/scala/almond/launcher/directives/JavaOptions.scala index 90b5100d9..cf6c8d622 100644 --- a/modules/scala/launcher/src/main/scala/almond/launcher/directives/JavaOptions.scala +++ b/modules/scala/shared-directives/src/main/scala/almond/launcher/directives/JavaOptions.scala @@ -1,7 +1,5 @@ package almond.launcher.directives -import almond.launcher.LauncherParameters - import scala.cli.directivehandler._ @DirectiveGroupName("Java options") @@ -21,5 +19,5 @@ final case class JavaOptions( } object JavaOptions { - val handler: DirectiveHandler[JavaOptions] = DirectiveHandler.derive + val handler: DirectiveHandler[JavaOptions] = DirectiveHandler.deriver[JavaOptions].derive } diff --git a/modules/scala/launcher/src/main/scala/almond/launcher/directives/Jvm.scala b/modules/scala/shared-directives/src/main/scala/almond/launcher/directives/Jvm.scala similarity index 85% rename from modules/scala/launcher/src/main/scala/almond/launcher/directives/Jvm.scala rename to modules/scala/shared-directives/src/main/scala/almond/launcher/directives/Jvm.scala index 611ea0705..9005f87ad 100644 --- a/modules/scala/launcher/src/main/scala/almond/launcher/directives/Jvm.scala +++ b/modules/scala/shared-directives/src/main/scala/almond/launcher/directives/Jvm.scala @@ -1,7 +1,5 @@ package almond.launcher.directives -import almond.launcher.LauncherParameters - import scala.cli.directivehandler._ @DirectiveGroupName("JVM version") @@ -22,5 +20,5 @@ final case class Jvm( } object Jvm { - val handler: DirectiveHandler[Jvm] = DirectiveHandler.derive + val handler: DirectiveHandler[Jvm] = DirectiveHandler.deriver[Jvm].derive } diff --git a/modules/scala/launcher/src/main/scala/almond/launcher/LauncherParameters.scala b/modules/scala/shared-directives/src/main/scala/almond/launcher/directives/LauncherParameters.scala similarity index 51% rename from modules/scala/launcher/src/main/scala/almond/launcher/LauncherParameters.scala rename to modules/scala/shared-directives/src/main/scala/almond/launcher/directives/LauncherParameters.scala index 3f6ad2989..41b4437a2 100644 --- a/modules/scala/launcher/src/main/scala/almond/launcher/LauncherParameters.scala +++ b/modules/scala/shared-directives/src/main/scala/almond/launcher/directives/LauncherParameters.scala @@ -1,4 +1,6 @@ -package almond.launcher +package almond.launcher.directives + +import scala.cli.directivehandler.{DirectiveHandler, DirectiveHandlers} final case class LauncherParameters( jvm: Option[String] = None, @@ -12,3 +14,15 @@ final case class LauncherParameters( scala.orElse(other.scala) ) } + +object LauncherParameters { + + val handlers = DirectiveHandlers( + Seq[DirectiveHandler[HasLauncherParameters]]( + JavaOptions.handler, + Jvm.handler, + ScalaVersion.handler + ) + ) + +} diff --git a/modules/scala/launcher/src/main/scala/almond/launcher/directives/ScalaVersion.scala b/modules/scala/shared-directives/src/main/scala/almond/launcher/directives/ScalaVersion.scala similarity index 92% rename from modules/scala/launcher/src/main/scala/almond/launcher/directives/ScalaVersion.scala rename to modules/scala/shared-directives/src/main/scala/almond/launcher/directives/ScalaVersion.scala index 3f05b4fdc..c1726b515 100644 --- a/modules/scala/launcher/src/main/scala/almond/launcher/directives/ScalaVersion.scala +++ b/modules/scala/shared-directives/src/main/scala/almond/launcher/directives/ScalaVersion.scala @@ -1,7 +1,5 @@ package almond.launcher.directives -import almond.launcher.LauncherParameters - import scala.cli.directivehandler._ @DirectiveGroupName("Scala version") @@ -22,5 +20,5 @@ final case class ScalaVersion( } object ScalaVersion { - val handler: DirectiveHandler[ScalaVersion] = DirectiveHandler.derive + val handler: DirectiveHandler[ScalaVersion] = DirectiveHandler.deriver[ScalaVersion].derive } 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 9fb19e5e7..532ab0ecf 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 @@ -742,4 +742,87 @@ object Tests { } } + def addDependency()(implicit + sessionId: SessionId, + runner: Runner + ): Unit = + runner.withSession() { implicit session => + execute( + """//> using dep "org.typelevel::cats-kernel:2.6.1" + |import cats.kernel._ + |val msg = + | Monoid.instance[String]("", (a, b) => a + b) + | .combineAll(List("Hello", "", "")) + |""".stripMargin, + """import cats.kernel._ + | + |msg: String = "Hello"""".stripMargin + ) + } + + def addRepository()(implicit + sessionId: SessionId, + runner: Runner + ): Unit = + runner.withSession() { implicit session => + // that repository should already have been added by Almond, so we don't test much here… + execute( + """//> using repository "jitpack" + |//> using dep "com.github.jupyter:jvm-repr:0.4.0" + |import jupyter._ + |""".stripMargin, + """import jupyter._ + |""".stripMargin + ) + } + + def addScalacOption(scalaVersion: String)(implicit + sessionId: SessionId, + runner: Runner + ): Unit = + runner.withSession() { implicit session => + execute( + """@deprecated("foo", "0.1") + |def getValue0(): Int = 2 + |val n0 = getValue0() + |""".stripMargin, + """defined function getValue0 + |n0: Int = 2""".stripMargin, + ignoreStreams = true + ) + + val errorMessage = + if (scalaVersion.startsWith("2.13.")) + """cell2.sc:3: method getValue in class Helper is deprecated (since 0.1): foo + |val n = getValue() + | ^ + |No warnings can be incurred under -Werror. + |Compilation Failed""".stripMargin + else if (scalaVersion.startsWith("2.12.")) + """cell2.sc:3: method getValue in class Helper is deprecated (since 0.1): foo + |val n = getValue() + | ^ + |No warnings can be incurred under -Xfatal-warnings. + |Compilation Failed""".stripMargin + else + """-- Error: cell2.sc:3:8 --------------------------------------------------------- + |3 |val n = getValue() + | | ^^^^^^^^ + | | method getValue in class Helper is deprecated since 0.1: foo + |Compilation Failed""".stripMargin + + execute( + """//> using option "-Xfatal-warnings" "-deprecation" + |@deprecated("foo", "0.1") + |def getValue(): Int = 2 + |val n = getValue() + |""".stripMargin, + expectError = true, + stderr = errorMessage, + errors = Seq( + ("", "Compilation Failed", List("Compilation Failed")) + ) + ) + } + } diff --git a/modules/shared/interpreter-api/src/main/scala/almond/interpreter/api/OutputHandler.scala b/modules/shared/interpreter-api/src/main/scala/almond/interpreter/api/OutputHandler.scala index 81499be3d..0d049cb90 100644 --- a/modules/shared/interpreter-api/src/main/scala/almond/interpreter/api/OutputHandler.scala +++ b/modules/shared/interpreter-api/src/main/scala/almond/interpreter/api/OutputHandler.scala @@ -29,6 +29,8 @@ abstract class OutputHandler extends OutputHandler.Helpers with OutputHandler.Up def display(displayData: DisplayData): Unit def canOutput(): Boolean + + def messageIdOpt: Option[String] } object OutputHandler { @@ -80,6 +82,8 @@ object OutputHandler { } def canOutput(): Boolean = false + + def messageIdOpt: Option[String] = None } final class StableOutputHandler(underlying: => OutputHandler) extends OutputHandler { @@ -93,6 +97,9 @@ object OutputHandler { underlying.updateDisplay(displayData) def canOutput(): Boolean = underlying.canOutput() + + def messageIdOpt: Option[String] = + underlying.messageIdOpt } object NopOutputHandler extends OutputHandler { @@ -101,6 +108,8 @@ object OutputHandler { def display(displayData: DisplayData): Unit = () def updateDisplay(displayData: DisplayData): Unit = () def canOutput(): Boolean = false + + def messageIdOpt: Option[String] = None } } diff --git a/modules/shared/interpreter/src/main/scala/almond/interpreter/messagehandlers/InterpreterMessageHandlers.scala b/modules/shared/interpreter/src/main/scala/almond/interpreter/messagehandlers/InterpreterMessageHandlers.scala index 669232407..90ec487f1 100644 --- a/modules/shared/interpreter/src/main/scala/almond/interpreter/messagehandlers/InterpreterMessageHandlers.scala +++ b/modules/shared/interpreter/src/main/scala/almond/interpreter/messagehandlers/InterpreterMessageHandlers.scala @@ -291,6 +291,8 @@ object InterpreterMessageHandlers { commHandlerOpt.foreach(_.updateDisplay(displayData)) def canOutput(): Boolean = true + + def messageIdOpt: Option[String] = Some(message.header.msg_id) } } diff --git a/modules/shared/test-kit/src/main/scala/almond/testkit/Dsl.scala b/modules/shared/test-kit/src/main/scala/almond/testkit/Dsl.scala index 2cf0d3212..d88c9066b 100644 --- a/modules/shared/test-kit/src/main/scala/almond/testkit/Dsl.scala +++ b/modules/shared/test-kit/src/main/scala/almond/testkit/Dsl.scala @@ -180,8 +180,10 @@ object Dsl { } else { if (stderr != stderrMessages) { - pprint.err.log(stderr) - pprint.err.log(stderrMessages) + val expectedStderr = stderr + val obtainedStderr = stderrMessages + pprint.err.log(expectedStderr) + pprint.err.log(obtainedStderr) } expect(stderr == stderrMessages) } @@ -192,13 +194,25 @@ object Dsl { .sortBy(_._1) .map(_._2) .map(s => if (trimReplyLines) s.trimLines else s) - if (replies != Option(reply).toVector) { - val expectedSingleReply = reply - val gotReplies = replies - pprint.err.log(expectedSingleReply) - pprint.err.log(gotReplies) + if (Properties.isWin) { + expect(replies.length == Option(reply).toVector.length) + val obtainedReplyLines = replies.headOption.iterator.flatMap(_.linesIterator).toVector + val expectedReplyLines = Option(reply).iterator.flatMap(_.linesIterator).toVector + if (obtainedReplyLines != expectedReplyLines) { + pprint.err.log(obtainedReplyLines) + pprint.err.log(expectedReplyLines) + } + expect(obtainedReplyLines == expectedReplyLines) + } + else { + if (replies != Option(reply).toVector) { + val expectedSingleReply = reply + val gotReplies = replies + pprint.err.log(expectedSingleReply) + pprint.err.log(gotReplies) + } + expect(replies == Option(reply).toVector) } - expect(replies == Option(reply).toVector) if (replyPayloads != null) { val gotReplyPayloads = streams.executeReplyPayloads diff --git a/modules/shared/test/src/main/scala/almond/interpreter/TestOutputHandler.scala b/modules/shared/test/src/main/scala/almond/interpreter/TestOutputHandler.scala index 3078406a7..532a5de80 100644 --- a/modules/shared/test/src/main/scala/almond/interpreter/TestOutputHandler.scala +++ b/modules/shared/test/src/main/scala/almond/interpreter/TestOutputHandler.scala @@ -36,6 +36,7 @@ final class TestOutputHandler extends OutputHandler { def result(): Seq[Output] = output.result() + def messageIdOpt: Option[String] = None } object TestOutputHandler { diff --git a/project/deps.sc b/project/deps.sc index e07bd1b5a..2657d77df 100644 --- a/project/deps.sc +++ b/project/deps.sc @@ -49,9 +49,10 @@ object Deps { def coursier = ivy"io.get-coursier::coursier:${Versions.coursier}" def coursierApi = ivy"io.get-coursier:interface:1.0.18" def coursierLauncher = ivy"io.get-coursier:coursier-launcher_2.13:${Versions.coursier}" - def directiveHandler = ivy"io.github.alexarchambault.scala-cli::directive-handler:0.1.1" - def expecty = ivy"com.eed3si9n.expecty::expecty:0.16.0" - def fansi = ivy"com.lihaoyi::fansi:0.4.0" + def dependencyInterface = ivy"io.get-coursier::dependency-interface:0.2.3" + def directiveHandler = ivy"io.github.alexarchambault.scala-cli::directive-handler:0.1.2" + def expecty = ivy"com.eed3si9n.expecty::expecty:0.16.0" + def fansi = ivy"com.lihaoyi::fansi:0.4.0" def fs2(sv: String) = if (sv.startsWith("2.")) ivy"co.fs2::fs2-core:3.7.0" else ivy"co.fs2:fs2-core_2.13:3.6.1" def jansi = ivy"org.fusesource.jansi:jansi:2.4.0"