pandoc-plot 0.7.0.0 → 0.7.1.0
raw patch · 15 files changed
+160/−139 lines, 15 filesdep +lifted-asyncdep −asyncdep −timedep ~criteriondep ~mtlPVP: major bump suggested
API removals or changes: PVP suggests a major version bump
Dependencies added: lifted-async
Dependencies removed: async, time
Dependency ranges changed: criterion, mtl
API changes (from Hackage documentation)
+ Text.Pandoc.Filter.Plot.Internal: RuntimeEnv :: Configuration -> Logger -> RuntimeEnv
+ Text.Pandoc.Filter.Plot.Internal: [envConfig] :: RuntimeEnv -> Configuration
+ Text.Pandoc.Filter.Plot.Internal: [envLogger] :: RuntimeEnv -> Logger
+ Text.Pandoc.Filter.Plot.Internal: asksConfig :: (Configuration -> a) -> PlotM a
+ Text.Pandoc.Filter.Plot.Internal: data RuntimeEnv
- Text.Pandoc.Filter.Plot: make :: Configuration -> Block -> IO Block
+ Text.Pandoc.Filter.Plot: make :: Block -> PlotM Block
- Text.Pandoc.Filter.Plot: makeEither :: Configuration -> Block -> IO (Either PandocPlotError Block)
+ Text.Pandoc.Filter.Plot: makeEither :: Block -> PlotM (Either PandocPlotError Block)
- Text.Pandoc.Filter.Plot.Internal: type PlotM a = ReaderT Configuration LoggingM a
+ Text.Pandoc.Filter.Plot.Internal: type PlotM a = ReaderT RuntimeEnv IO a
Files
- CHANGELOG.md +6/−0
- README.md +1/−1
- benchmark/MatplotlibGallery.hs +4/−4
- benchmark/bench.hs +11/−3
- pandoc-plot.cabal +5/−5
- src/Text/Pandoc/Filter/Plot.hs +21/−26
- src/Text/Pandoc/Filter/Plot/Monad.hs +42/−19
- src/Text/Pandoc/Filter/Plot/Monad/Logging.hs +45/−54
- src/Text/Pandoc/Filter/Plot/Monad/Types.hs +0/−1
- src/Text/Pandoc/Filter/Plot/Parse.hs +1/−1
- src/Text/Pandoc/Filter/Plot/Renderers.hs +3/−4
- src/Text/Pandoc/Filter/Plot/Renderers/Matlab.hs +1/−1
- src/Text/Pandoc/Filter/Plot/Renderers/Prelude.hs +9/−9
- stack.yaml +1/−1
- tests/Common.hs +10/−10
CHANGELOG.md view
@@ -2,8 +2,14 @@ pandoc-plot uses [Semantic Versioning](http://semver.org/spec/v2.0.0.html) +Release 0.7.1.0 +--------------- + +* Better multi-threaded logging. Only one thread (the logging thread) performs IO on the log file. This prevents hang-ups when working on large documents. + Release 0.7.0.0 --------------- + * Added documentation on using `pandoc-plot` with LaTeX documents as well. * Added preliminary support for logging to `pandoc-plot`. You can turn on this feature in the configuration as follows:
README.md view
@@ -1,5 +1,5 @@ <!-- -The file README.md is automatically generated by the tools/mkreadme script +The file README.md is automatically generated by the mkreadme.ps1 script Do not edit manually -->
benchmark/MatplotlibGallery.hs view
@@ -19,8 +19,8 @@ strToExp :: String -> Q Exp strToExp s = return $ VarE 'fromString `AppE` LitE (StringL s) -galleryItem1 :: Q Exp +galleryItem1, galleryItem2, galleryItem3, galleryItem4 :: Q Exp galleryItem1 = galleryItem "benchmark/gallery_item_1.py" - -galleryItem2 :: Q Exp -galleryItem2 = galleryItem "benchmark/gallery_item_2.py"+galleryItem2 = galleryItem "benchmark/gallery_item_2.py" +galleryItem3 = galleryItem "benchmark/gallery_item_3.py" +galleryItem4 = galleryItem "benchmark/gallery_item_4.py"
benchmark/bench.hs view
@@ -6,19 +6,25 @@ import Text.Pandoc.Filter.Plot import Text.Pandoc.Filter.Plot.Internal -import MatplotlibGallery (galleryItem1, galleryItem2) +import MatplotlibGallery ( galleryItem1, galleryItem2 + , galleryItem3, galleryItem4 + ) main :: IO () main = defaultMain [ envWithCleanup (return ()) (\_ -> cleanupEnv) $ \_ -> bgroup "main" [ - bench "filter" $ nfIO (plotTransform defaultConfiguration benchDoc) + bench "filter-async" $ nfIO (plotTransform plotConfig benchDoc) + , bench "filter" $ nfIO (makePlot plotConfig benchDoc) ] ] +plotConfig :: Configuration +plotConfig = defaultConfiguration {logVerbosity=Silent, logSink =StdErr} + cleanupEnv :: IO () -cleanupEnv = cleanOutputDirs defaultConfiguration benchDoc >> return () +cleanupEnv = cleanOutputDirs plotConfig benchDoc >> return () codeBlock :: Script -> Block @@ -28,4 +34,6 @@ benchDoc :: Pandoc benchDoc = Pandoc mempty [ codeBlock $(galleryItem1) , codeBlock $(galleryItem2) + , codeBlock $(galleryItem3) + , codeBlock $(galleryItem4) ]
pandoc-plot.cabal view
@@ -1,5 +1,5 @@ name: pandoc-plot -version: 0.7.0.0 +version: 0.7.1.0 cabal-version: >= 1.12 synopsis: A Pandoc filter to include figures generated from code blocks using your plotting toolkit of choice. description: A Pandoc filter to include figures generated from code blocks. Keep the document and code in the same location. Output is captured and included as a figure. @@ -12,6 +12,7 @@ license: GPL-2 license-file: LICENSE build-type: Simple +tested-with: GHC == 8.6.5, GHC == 8.8.3 extra-source-files: CHANGELOG.md LICENSE @@ -61,13 +62,12 @@ , hashable >= 1 && < 2 , pandoc >= 2.8 && < 3 , pandoc-types >= 1.20 && < 2 - , async >= 2 && < 3 + , lifted-async >= 0.10 && < 1 , shakespeare >= 2.0 && < 3 , text >= 1 && < 2 - , time >= 1 && < 2 , typed-process >= 0.2.1 && < 1 , yaml >= 0.8 && < 1 - , mtl >= 2.2 && < 2.3 + , mtl >= 2.2 && < 3 default-language: Haskell2010 executable pandoc-plot @@ -122,7 +122,7 @@ build-depends: base , pandoc-plot , pandoc-types - , criterion >= 1.0 && < 1.6 + , criterion >= 1.0 && < 2 , template-haskell > 2.7 && < 3 , text ghc-options: -Wall -Wcompat -rtsopts -O2 -threaded -with-rtsopts=-N
src/Text/Pandoc/Filter/Plot.hs view
@@ -89,8 +89,7 @@ , readDoc ) where -import Control.Concurrent.Async (mapConcurrently) - +import Control.Concurrent.Async.Lifted (mapConcurrently) import Data.Text (Text, unpack) import Text.Pandoc.Definition (Pandoc(..), Block) @@ -108,8 +107,8 @@ plotTransform :: Configuration -- ^ Configuration for default values -> Pandoc -- ^ Input document -> IO Pandoc -plotTransform conf (Pandoc meta blocks) = - mapConcurrently (makePlot conf) blocks >>= return . Pandoc meta +plotTransform conf (Pandoc meta blocks) = + runPlotM conf $ mapConcurrently make blocks >>= return . Pandoc meta -- | Highest-level function that can be walked over a Pandoc tree. @@ -127,33 +126,29 @@ => Configuration -- ^ Configuration for default values -> a -- ^ Input block or document -> IO a -makePlot conf = walkM (make conf) +makePlot conf = runPlotM conf . walkM make -make :: Configuration - -> Block - -> IO Block -make conf blk = - either (const (return blk) ) return =<< makeEither conf blk +-- | Try to process the block with `pandoc-plot`. If a failure happens (or the block) +-- was not meant to become a figure, return the block as-is. +make :: Block -> PlotM Block +make blk = either (const (return blk) ) return =<< makeEither blk -makeEither :: Configuration - -> Block - -> IO (Either PandocPlotError Block) -makeEither conf block = runPlotM conf (go block) +-- | Try to process the block with `pandoc-plot`, documenting the error. +makeEither :: Block -> PlotM (Either PandocPlotError Block) +makeEither block = + parseFigureSpec block + >>= maybe + (return $ Right block) + (\s -> runScriptIfNecessary s >>= handleResult s) where - go :: Block -> PlotM (Either PandocPlotError Block) - go blk = parseFigureSpec blk - >>= maybe - (return $ Right blk) - (\s -> runScriptIfNecessary s >>= handleResult s) - where - -- Logging of errors has been taken care of in @runScriptIfNecessary@ - handleResult :: FigureSpec -> ScriptResult -> PlotM (Either PandocPlotError Block) - handleResult spec ScriptSuccess = return $ Right $ toImage (captionFormat conf) spec - handleResult _ (ScriptFailure msg code) = return $ Left (ScriptRuntimeError msg code) - handleResult _ (ScriptChecksFailed msg) = return $ Left (ScriptChecksFailedError msg) - handleResult _ (ToolkitNotInstalled tk') = return $ Left (ToolkitNotInstalledError tk') + -- Logging of errors has been taken care of in @runScriptIfNecessary@ + handleResult :: FigureSpec -> ScriptResult -> PlotM (Either PandocPlotError Block) + handleResult _ (ScriptFailure msg code) = return $ Left (ScriptRuntimeError msg code) + handleResult _ (ScriptChecksFailed msg) = return $ Left (ScriptChecksFailedError msg) + handleResult _ (ToolkitNotInstalled tk') = return $ Left (ToolkitNotInstalledError tk') + handleResult spec ScriptSuccess = asks envConfig >>= \c -> return $ Right $ toImage (captionFormat c) spec data PandocPlotError
src/Text/Pandoc/Filter/Plot/Monad.hs view
@@ -13,6 +13,7 @@ module Text.Pandoc.Filter.Plot.Monad ( Configuration(..) , PlotM + , RuntimeEnv(..) , runPlotM -- * Running external commands , runCommand @@ -27,14 +28,19 @@ , liftIO , ask , asks + , asksConfig -- * Base types , module Text.Pandoc.Filter.Plot.Monad.Types ) where + +import Control.Concurrent.Chan (writeChan) + import Control.Monad.Reader import Data.ByteString.Lazy (toStrict) import Data.Text (Text, pack, unpack) +import qualified Data.Text as T import Data.Text.Encoding (decodeUtf8With) import Data.Text.Encoding.Error (lenientDecode) @@ -47,39 +53,56 @@ import Prelude hiding (log, fst, snd) -import Text.Pandoc.Filter.Plot.Monad.Logging +import Text.Pandoc.Filter.Plot.Monad.Logging as Log import Text.Pandoc.Filter.Plot.Monad.Types + -- | pandoc-plot monad -type PlotM a = ReaderT Configuration LoggingM a +type PlotM a = ReaderT RuntimeEnv IO a --- | Evaluate a @PlotM@ action +data RuntimeEnv = + RuntimeEnv { envConfig :: Configuration + , envLogger :: Logger + } + +-- | Get access to configuration within the @PlotM@ monad. +asksConfig :: (Configuration -> a) -> PlotM a +asksConfig f = asks (f . envConfig) + + +-- | Evaluate a @PlotM@ action. runPlotM :: Configuration -> PlotM a -> IO a runPlotM conf v = let verbosity = logVerbosity conf sink = logSink conf - in runLoggingM verbosity sink (runReaderT v conf) - - -debug :: Text -> PlotM () -debug t = lift $ log "DEBUG| " Debug t - - -err :: Text -> PlotM () -err t = lift $ log "ERROR| " Error t + in withLogger verbosity sink $ + \logger -> runReaderT v (RuntimeEnv conf logger) -warning :: Text -> PlotM () -warning t = lift $ log "WARN | " Warning t +debug, err, warning, info :: Text -> PlotM () +debug = log "DEBUG| " Debug +err = log "ERROR| " Error +warning = log "WARN | " Warning +info = log "INFO | " Info -info :: Text -> PlotM () -info t = lift $ log "INFO | " Info t +-- | General purpose logging. +log :: Text -- ^ Header. + -> Verbosity -- ^ Verbosity of the message. + -> Text -- ^ Message (can be multiple lines). + -> PlotM () +log h v t = do + logger <- asks envLogger + when (v >= lVerbosity logger) $ + liftIO $ do + let lines' = [l' | l' <- T.lines t] + forM_ lines' $ \l -> writeChan (lChannel logger) (Just (h <> l <> "\n")) --- | Run a command within the @PlotM@ monad. Stdout and Stderr --- are read and decoded. Logging happens at the debug level. +-- | Run a command within the @PlotM@ monad. Stderr stream +-- is read and decoded, while Stdout is ignored. +-- Logging happens at the debug level. runCommand :: Text -> PlotM (ExitCode, Text) runCommand command = do (ec, processOutput') <- liftIO @@ -108,7 +131,7 @@ -- --- -- title: My document -- author: John Doe --- plot-configuration: /path/to/file.yml +-- plot-configuration: path\to\file.yml -- --- -- @ --
src/Text/Pandoc/Filter/Plot/Monad/Logging.hs view
@@ -15,27 +15,25 @@ module Text.Pandoc.Filter.Plot.Monad.Logging ( Verbosity(..) , LogSink(..) - , LoggingM - , runLoggingM - , log + , Logger(..) + , withLogger ) where -import Control.Monad.Trans (liftIO) -import Control.Monad.Writer.Strict (WriterT, runWriterT, tell) +import Control.Concurrent (forkIO) +import Control.Concurrent.Chan (Chan, newChan, readChan, writeChan) +import Control.Concurrent.MVar (MVar, newEmptyMVar, putMVar, takeMVar) -import Data.Char (toLower) -import Data.List (sortOn) -import Data.String (IsString(..)) -import Data.Text (Text, unpack) -import qualified Data.Text as T -import Data.Text.IO (hPutStr) -import Data.Time.Clock.System (getSystemTime, SystemTime(..)) +import Control.Monad (forever) + +import Data.Char (toLower) +import Data.String (IsString(..)) +import Data.Text (Text, unpack) +import Data.Text.IO (hPutStr) import Data.Yaml -import System.IO (stderr, withFile, IOMode (AppendMode) ) +import System.IO (stderr, withFile, IOMode (AppendMode) ) -import Prelude hiding (log, fst, snd) -- | Verbosity of the logger. @@ -52,40 +50,45 @@ | LogFile FilePath -- ^ Appended to file. deriving (Eq, Show) -type LogMessage = (Verbosity, SystemTime, Text) -type LoggingM = WriterT [LogMessage] IO +-- | The logging implementation is very similar to Hakyll's. +data Logger = Logger + { lVerbosity :: Verbosity + , lChannel :: Chan (Maybe Text) + , lSink :: Text -> IO () + , lSync :: MVar () + } -runLoggingM :: Verbosity -> LogSink -> LoggingM a -> IO a -runLoggingM Silent _ = runLoggingM' Silent $ mapM_ (return . trd) -runLoggingM v StdErr = runLoggingM' v $ mapM_ (hPutStr stderr . trd) -runLoggingM v (LogFile fp) = runLoggingM' v $ mapM_ (\m -> withFile fp AppendMode $ \h -> hPutStr h (trd m)) +-- | Perform an IO action with a logger. Using this function +-- ensures that logging will be gracefully shut down. +withLogger :: Verbosity -> LogSink -> (Logger -> IO a) -> IO a +withLogger v s f = do + logger <- Logger <$> pure v + <*> newChan + <*> pure (sink s) + <*> newEmptyMVar + -- The logger either logs messages (if Just "message"), + -- or stops working on Nothing. + _ <- forkIO $ forever $ + readChan (lChannel logger) + >>= maybe (putMVar (lSync logger) ()) (lSink logger) -runLoggingM' :: Verbosity -- ^ Minimum verbosity to keep - -> ([LogMessage] -> IO ()) -- ^ Log sink - -> LoggingM a - -> IO a -runLoggingM' v f m = do - (r, t) <- runWriterT m - -- Messages with lower level than minimum are discarded - -- We also re-order messages to be chronological - let t' = sortOn snd $ filter (\message -> fst message >= v) t - liftIO $ f t' - return r + result <- f logger + + -- Flushing the logger + -- To signal to the logger that logging duties are over, + -- we append Nothing to the channel, and wait for it to finish + -- dealing with all items in the channel. + writeChan (lChannel logger) Nothing + () <- takeMVar (lSync logger) + return result --- | General logging function. --- Input text will be decomposed into lines, with each --- line becoming a log line. -log :: Text -- ^ Header - -> Verbosity - -> Text -- ^ Message - -> LoggingM () -log h v t = do - timestamp <- liftIO $ getSystemTime - tell [(v, timestamp, h <> l <> "\n") | l <- T.lines t] + where + sink StdErr = hPutStr stderr + sink (LogFile fp) = \t -> withFile fp AppendMode $ \h -> hPutStr h t instance IsString Verbosity where @@ -101,16 +104,4 @@ instance FromJSON Verbosity where parseJSON (String t) = pure $ fromString . unpack $ t - parseJSON _ = fail $ "Could not parse the logging verbosity." - - -fst :: (a,b,c) -> a -fst (a,_,_) = a - - -snd :: (a,b,c) -> b -snd (_,b,_) = b - - -trd :: (a,b,c) -> c -trd (_,_,c) = c+ parseJSON _ = fail $ "Could not parse the logging verbosity."
src/Text/Pandoc/Filter/Plot/Monad/Types.hs view
@@ -33,7 +33,6 @@ import Data.Char (toLower) import Data.Hashable (hash) import Data.List (intersperse) -import Data.Semigroup (Semigroup (..)) import Data.String (IsString (..)) import Data.Text (Text) import Data.Yaml
src/Text/Pandoc/Filter/Plot/Parse.hs view
@@ -69,7 +69,7 @@ figureSpec :: Toolkit -> PlotM FigureSpec figureSpec toolkit = do - conf <- ask + conf <- asks envConfig let extraAttrs' = parseExtraAttrs toolkit attrs' header = comment toolkit $ "Generated by pandoc-plot " <> ((pack . showVersion) version) defaultPreamble = preambleSelector toolkit conf
src/Text/Pandoc/Filter/Plot/Renderers.hs view
@@ -30,7 +30,7 @@ , OutputSpec(..) ) where -import Control.Concurrent.Async (forConcurrently) +import Control.Concurrent.Async.Lifted (forConcurrently) import Data.List ((\\)) import Data.Map.Strict (Map) @@ -175,9 +175,8 @@ -- | Monadic version of @availableToolkits@. availableToolkitsM :: PlotM [Toolkit] availableToolkitsM = do - conf <- ask - mtks <- liftIO $ forConcurrently toolkits $ \tk -> do - available <- runPlotM conf $ toolkitAvailable tk + mtks <- forConcurrently toolkits $ \tk -> do + available <- toolkitAvailable tk if available then return $ Just tk else return Nothing
src/Text/Pandoc/Filter/Plot/Renderers/Matlab.hs view
@@ -40,7 +40,7 @@ -- Therefore, we cannot rely on this behavior to know if matlab is present, -- like other toolkits. matlabAvailable :: PlotM Bool -matlabAvailable = asks matlabExe >>= (\exe -> liftIO $ existsOnPath (exe <> exeExtension)) +matlabAvailable = asksConfig matlabExe >>= (\exe -> liftIO $ existsOnPath (exe <> exeExtension)) matlabCapture :: FigureSpec -> FilePath -> Script
src/Text/Pandoc/Filter/Plot/Renderers/Prelude.hs view
@@ -56,15 +56,15 @@ -- | Path to the executable of a toolkit. If the executable can -- be found, then it will be the full path to it. executable :: Toolkit -> PlotM FilePath -executable Matplotlib = asks matplotlibExe >>= liftIO . tryToFindExe -executable PlotlyPython = asks plotlyPythonExe >>= liftIO . tryToFindExe -executable PlotlyR = asks plotlyRExe >>= liftIO . tryToFindExe -executable Matlab = asks matlabExe >>= liftIO . tryToFindExe -executable Mathematica = asks mathematicaExe >>= liftIO . tryToFindExe -executable Octave = asks octaveExe >>= liftIO . tryToFindExe -executable GGPlot2 = asks ggplot2Exe >>= liftIO . tryToFindExe -executable GNUPlot = asks gnuplotExe >>= liftIO . tryToFindExe -executable Graphviz = asks graphvizExe >>= liftIO . tryToFindExe +executable Matplotlib = asksConfig matplotlibExe >>= liftIO . tryToFindExe +executable PlotlyPython = asksConfig plotlyPythonExe >>= liftIO . tryToFindExe +executable PlotlyR = asksConfig plotlyRExe >>= liftIO . tryToFindExe +executable Matlab = asksConfig matlabExe >>= liftIO . tryToFindExe +executable Mathematica = asksConfig mathematicaExe >>= liftIO . tryToFindExe +executable Octave = asksConfig octaveExe >>= liftIO . tryToFindExe +executable GGPlot2 = asksConfig ggplot2Exe >>= liftIO . tryToFindExe +executable GNUPlot = asksConfig gnuplotExe >>= liftIO . tryToFindExe +executable Graphviz = asksConfig graphvizExe >>= liftIO . tryToFindExe -- | Internal description of all information
stack.yaml view
@@ -1,4 +1,4 @@-resolver: lts-15.16 # GHC 8.8.3 +resolver: lts-16.0 # GHC 8.8.3 packages: - .
tests/Common.hs view
@@ -43,7 +43,7 @@ ensureDirectoryExistsAndEmpty tempDir let cb = (addDirectory tempDir $ codeBlock tk (trivialContent tk)) - _ <- make defaultTestConfig cb + _ <- runPlotM defaultTestConfig $ make cb filesCreated <- length <$> listDirectory tempDir assertEqual "" 2 filesCreated @@ -58,7 +58,7 @@ let cb = (addPreamble (include tk) $ addDirectory tempDir $ codeBlock tk (trivialContent tk)) - _ <- make defaultTestConfig cb + _ <- runPlotM defaultTestConfig $ make cb inclusion <- readFile (include tk) sourcePath <- head . filter (isExtensionOf "txt") <$> listDirectory tempDir src <- readFile (tempDir </> sourcePath) @@ -84,7 +84,7 @@ let fmt = head (supportedSaveFormats tk) cb = (addSaveFormat fmt $ addDirectory tempDir $ codeBlock tk (trivialContent tk)) - _ <- make defaultTestConfig cb + _ <- runPlotM defaultTestConfig $ make cb numberjpgFiles <- length <$> filter (isExtensionOf (extension fmt)) <$> listDirectory tempDir @@ -108,8 +108,8 @@ $ addDirectory tempDir $ addCaption expected $ codeBlock tk (trivialContent tk) - blockNoSource <- make defaultTestConfig noSource - blockWithSource <- make defaultTestConfig withSource + blockNoSource <- runPlotM defaultTestConfig $ make noSource + blockWithSource <- runPlotM defaultTestConfig $ make withSource -- In the case where source=false, the caption is used verbatim. -- Otherwise, links will be appended to the caption; hence, the caption @@ -148,7 +148,7 @@ let cb = addDirectory tempDir $ addSaveFormat PNG $ codeBlock tk (trivialContent tk) - _ <- make config cb + _ <- runPlotM config $ make cb numberPngFiles <- length <$> filter (isExtensionOf (extension PNG)) <$> @@ -175,7 +175,7 @@ $ addCaption "**caption**" $ codeBlock tk (trivialContent tk) fmt = B.Format "markdown" - result <- make (defaultTestConfig {captionFormat=fmt}) cb + result <- runPlotM (defaultTestConfig {captionFormat=fmt}) $ make cb assertIsInfix expected (extractCaption result) where extractCaption (B.Para blocks) = extractImageCaption . head $ blocks @@ -200,7 +200,7 @@ $ addCaption "[title](https://google.com)" $ codeBlock tk (trivialContent tk) fmt = B.Format "markdown" - result <- make (defaultTestConfig {captionFormat=fmt}) cb + result <- runPlotM (defaultTestConfig {captionFormat=fmt}) $ make cb assertIsInfix expected (extractCaption result) where extractCaption (B.Para blocks) = extractImageCaption . head $ blocks @@ -222,7 +222,7 @@ let cb = addDirectory tempDir $ codeBlock tk (trivialContent tk) - result <- make defaultTestConfig cb + result <- runPlotM defaultTestConfig $ make cb cleanedDirs <- cleanOutputDirs defaultTestConfig cb assertEqual "" [tempDir] cleanedDirs @@ -244,7 +244,7 @@ ensureDirectoryExistsAndEmpty tempDir let cb = addDirectory tempDir $ codeBlock Matplotlib "plt.show()" - result <- makeEither defaultTestConfig cb + result <- runPlotM defaultTestConfig $ makeEither cb let expectedCheck :: Either PandocPlotError a -> Bool expectedCheck (Left (ScriptChecksFailedError _)) = True expectedCheck _ = False