PageRenderTime 31ms CodeModel.GetById 33ms RepoModel.GetById 1ms app.codeStats 0ms

/ipython-kernel/src/IHaskell/IPython/EasyKernel.hs

https://gitlab.com/stallmanifold/IHaskell
Haskell | 237 lines | 163 code | 29 blank | 45 comment | 1 complexity | cefffc23586a628b3b3b5fb439419fea MD5 | raw file
  1. {-# LANGUAGE OverloadedStrings #-}
  2. -- | Description : Easy IPython kernels = Overview This module provides automation for writing
  3. -- simple IPython kernels. In particular, it provides a record type that defines configurations and
  4. -- a function that interprets a configuration as an action in some monad that can do IO.
  5. --
  6. -- The configuration consists primarily of functions that implement the various features of a
  7. -- kernel, such as running code, looking up documentation, and performing completion. An example for
  8. -- a simple language that nevertheless has side effects, global state, and timing effects is
  9. -- included in the examples directory.
  10. --
  11. -- = Kernel Specs
  12. --
  13. -- To run your kernel, you will need to install the kernelspec into the Jupyter namespace. If your
  14. -- kernel name is `kernel`, you will need to run the command:
  15. --
  16. -- > kernel install
  17. --
  18. -- This will inform Jupyter of the kernel so that it may be used.
  19. --
  20. -- == Further profile improvements Consult the IPython documentation along with the generated
  21. -- profile source code for further configuration of the frontend, including syntax highlighting,
  22. -- logos, help text, and so forth.
  23. module IHaskell.IPython.EasyKernel (easyKernel, installKernelspec, KernelConfig(..)) where
  24. import Data.Aeson (decode, encode)
  25. import qualified Data.ByteString.Lazy as BL
  26. import System.IO.Temp (withTempDirectory)
  27. import System.Process (rawSystem)
  28. import Control.Concurrent (MVar, readChan, writeChan, newMVar, readMVar, modifyMVar_)
  29. import Control.Monad.IO.Class (MonadIO(..))
  30. import Control.Monad (forever, when, unless, void)
  31. import qualified Data.Map as Map
  32. import Data.Maybe (fromMaybe)
  33. import qualified Data.Text as T
  34. import IHaskell.IPython.Kernel
  35. import IHaskell.IPython.Message.UUID as UUID
  36. import IHaskell.IPython.Types
  37. import System.Directory (createDirectoryIfMissing, doesDirectoryExist, doesFileExist,
  38. getHomeDirectory, getTemporaryDirectory)
  39. import System.FilePath ((</>))
  40. import System.Exit (exitSuccess)
  41. import System.IO (openFile, IOMode(ReadMode))
  42. -- | The kernel configuration specifies the behavior that is specific to your language. The type
  43. -- parameters provide the monad in which your kernel will run, the type of intermediate outputs from
  44. -- running cells, and the type of final results of cells, respectively.
  45. data KernelConfig m output result =
  46. KernelConfig
  47. {
  48. -- | Info on the language of the kernel.
  49. kernelLanguageInfo :: LanguageInfo
  50. -- | Write all the files into the kernel directory, including `kernel.js`, `logo-64x64.png`, and any
  51. -- other required files. The directory to write to will be passed to this function, and the return
  52. -- value should be the kernelspec to be written to `kernel.json`.
  53. , writeKernelspec :: FilePath -> IO KernelSpec
  54. -- | How to render intermediate output
  55. , displayOutput :: output -> [DisplayData]
  56. -- | How to render final cell results
  57. , displayResult :: result -> [DisplayData]
  58. -- | Perform completion. The returned tuple consists of the matched text and completions. The
  59. -- arguments are the code in the cell and the position of the cursor in the cell.
  60. , completion :: T.Text -> Int -> m (T.Text, [T.Text])
  61. -- | Return the information or documentation for its argument, described by the cell contents and
  62. -- cursor position. The returned value is simply the data to display.
  63. , inspectInfo :: T.Text -> Int -> m (Maybe [DisplayData])
  64. -- | Execute a cell. The arguments are the contents of the cell, an IO action that will clear the
  65. -- current intermediate output, and an IO action that will add a new item to the intermediate
  66. -- output. The result consists of the actual result, the status to be sent to IPython, and the
  67. -- contents of the pager. Return the empty string to indicate that there is no pager output. Errors
  68. -- should be handled by defining an appropriate error constructor in your result type.
  69. , run :: T.Text -> IO () -> (output -> IO ()) -> m (result, ExecuteReplyStatus, String)
  70. , debug :: Bool -- ^ Whether to print extra debugging information to
  71. }
  72. -- Install the kernelspec, using the `writeKernelspec` field of the kernel configuration.
  73. installKernelspec :: MonadIO m
  74. => KernelConfig m output result -- ^ Kernel configuration to install
  75. -> Bool -- ^ Whether to use Jupyter `--replace`
  76. -> Maybe FilePath -- ^ (Optional) prefix to install into for Jupyter `--prefix`
  77. -> m ()
  78. installKernelspec config replace installPrefixMay =
  79. liftIO $ withTmpDir $ \tmp -> do
  80. let kernelDir = tmp </> languageName (kernelLanguageInfo config)
  81. createDirectoryIfMissing True kernelDir
  82. kernelSpec <- writeKernelspec config kernelDir
  83. let filename = kernelDir </> "kernel.json"
  84. BL.writeFile filename $ encode $ toJSON kernelSpec
  85. let replaceFlag = ["--replace" | replace]
  86. installPrefixFlag = maybe ["--user"] (\prefix -> ["--prefix", prefix]) installPrefixMay
  87. cmd = concat [["kernelspec", "install"], installPrefixFlag, [kernelDir], replaceFlag]
  88. void $ rawSystem "ipython" cmd
  89. where
  90. withTmpDir act = do
  91. tmp <- getTemporaryDirectory
  92. withTempDirectory tmp "easyKernel" act
  93. getProfile :: FilePath -> IO Profile
  94. getProfile fn = do
  95. profData <- openFile fn ReadMode >>= BL.hGetContents
  96. case decode profData of
  97. Just prof -> return prof
  98. Nothing -> error "Invalid profile data"
  99. createReplyHeader :: MonadIO m => MessageHeader -> m MessageHeader
  100. createReplyHeader parent = do
  101. -- Generate a new message UUID.
  102. newMessageId <- liftIO UUID.random
  103. let repType = fromMaybe err (replyType $ msgType parent)
  104. err = error $ "No reply for message " ++ show (msgType parent)
  105. return
  106. MessageHeader
  107. { identifiers = identifiers parent
  108. , parentHeader = Just parent
  109. , metadata = Map.fromList []
  110. , messageId = newMessageId
  111. , sessionId = sessionId parent
  112. , username = username parent
  113. , msgType = repType
  114. }
  115. -- | Execute an IPython kernel for a config. Your 'main' action should call this as the last thing
  116. -- it does.
  117. easyKernel :: MonadIO m
  118. => FilePath -- ^ The connection file provided by the IPython frontend
  119. -> KernelConfig m output result -- ^ The kernel configuration specifying how to react to
  120. -- messages
  121. -> m ()
  122. easyKernel profileFile config = do
  123. prof <- liftIO $ getProfile profileFile
  124. zmq@(Channels shellReqChan shellRepChan ctrlReqChan ctrlRepChan iopubChan _) <- liftIO $ serveProfile
  125. prof
  126. False
  127. execCount <- liftIO $ newMVar 0
  128. forever $ do
  129. req <- liftIO $ readChan shellReqChan
  130. repHeader <- createReplyHeader (header req)
  131. when (debug config) . liftIO $ print req
  132. reply <- replyTo config execCount zmq req repHeader
  133. liftIO $ writeChan shellRepChan reply
  134. replyTo :: MonadIO m
  135. => KernelConfig m output result
  136. -> MVar Integer
  137. -> ZeroMQInterface
  138. -> Message
  139. -> MessageHeader
  140. -> m Message
  141. replyTo config _ _ KernelInfoRequest{} replyHeader =
  142. return
  143. KernelInfoReply
  144. { header = replyHeader
  145. , languageInfo = kernelLanguageInfo config
  146. , implementation = "ipython-kernel.EasyKernel"
  147. , implementationVersion = "0.0"
  148. }
  149. replyTo config _ interface ShutdownRequest { restartPending = pending } replyHeader = do
  150. liftIO $ writeChan (shellReplyChannel interface) $ ShutdownReply replyHeader pending
  151. liftIO exitSuccess
  152. replyTo config execCount interface req@ExecuteRequest { getCode = code } replyHeader = do
  153. let send = writeChan (iopubChannel interface)
  154. busyHeader <- dupHeader replyHeader StatusMessage
  155. liftIO . send $ PublishStatus busyHeader Busy
  156. outputHeader <- dupHeader replyHeader DisplayDataMessage
  157. (res, replyStatus, pagerOut) <- let clearOutput = do
  158. clearHeader <- dupHeader replyHeader
  159. ClearOutputMessage
  160. send $ ClearOutput clearHeader False
  161. sendOutput x =
  162. send $ PublishDisplayData
  163. outputHeader
  164. (languageName $ kernelLanguageInfo
  165. config)
  166. (displayOutput config x)
  167. in run config code clearOutput sendOutput
  168. liftIO . send $ PublishDisplayData outputHeader (languageName $ kernelLanguageInfo config)
  169. (displayResult config res)
  170. idleHeader <- dupHeader replyHeader StatusMessage
  171. liftIO . send $ PublishStatus idleHeader Idle
  172. liftIO $ modifyMVar_ execCount (return . (+ 1))
  173. counter <- liftIO $ readMVar execCount
  174. return
  175. ExecuteReply
  176. { header = replyHeader
  177. , pagerOutput = [DisplayData PlainText $ T.pack pagerOut]
  178. , executionCounter = fromIntegral counter
  179. , status = replyStatus
  180. }
  181. replyTo config _ _ req@CompleteRequest{} replyHeader = do
  182. let code = getCode req
  183. pos = getCursorPos req
  184. (matchedText, completions) <- completion config code pos
  185. let start = pos - T.length matchedText
  186. end = pos
  187. reply = CompleteReply replyHeader completions start end Map.empty True
  188. return reply
  189. replyTo config _ _ req@InspectRequest{} replyHeader = do
  190. result <- inspectInfo config (inspectCode req) (inspectCursorPos req)
  191. let reply =
  192. case result of
  193. Just datas -> InspectReply
  194. { header = replyHeader
  195. , inspectStatus = True
  196. , inspectData = datas
  197. }
  198. _ -> InspectReply { header = replyHeader, inspectStatus = False, inspectData = [] }
  199. return reply
  200. replyTo _ _ _ msg _ = do
  201. liftIO $ putStrLn "Unknown message: "
  202. liftIO $ print msg
  203. return msg
  204. dupHeader :: MonadIO m => MessageHeader -> MessageType -> m MessageHeader
  205. dupHeader hdr mtype =
  206. do
  207. uuid <- liftIO UUID.random
  208. return hdr { messageId = uuid, msgType = mtype }