pandoc-pyplot 1.1.0.0 → 2.0.0.0
raw patch · 10 files changed
+685/−472 lines, 10 filesdep +hashabledep +randomdep +textPVP ok
version bump matches the API change (PVP)
Dependencies added: hashable, random, text
API changes (from Hackage documentation)
- Text.Pandoc.Filter.Pyplot: MissingDirectoryError :: FilePath -> PandocPyplotError
- Text.Pandoc.Filter.Scripting: addPlotCapture :: FilePath -> PythonScript -> PythonScript
+ Text.Pandoc.Filter.FigureSpec: EPS :: SaveFormat
+ Text.Pandoc.Filter.FigureSpec: FigureSpec :: String -> PythonScript -> SaveFormat -> FilePath -> Int -> Attr -> FigureSpec
+ Text.Pandoc.Filter.FigureSpec: JPG :: SaveFormat
+ Text.Pandoc.Filter.FigureSpec: PDF :: SaveFormat
+ Text.Pandoc.Filter.FigureSpec: PNG :: SaveFormat
+ Text.Pandoc.Filter.FigureSpec: SVG :: SaveFormat
+ Text.Pandoc.Filter.FigureSpec: [blockAttrs] :: FigureSpec -> Attr
+ Text.Pandoc.Filter.FigureSpec: [caption] :: FigureSpec -> String
+ Text.Pandoc.Filter.FigureSpec: [directory] :: FigureSpec -> FilePath
+ Text.Pandoc.Filter.FigureSpec: [dpi] :: FigureSpec -> Int
+ Text.Pandoc.Filter.FigureSpec: [saveFormat] :: FigureSpec -> SaveFormat
+ Text.Pandoc.Filter.FigureSpec: [script] :: FigureSpec -> PythonScript
+ Text.Pandoc.Filter.FigureSpec: addPlotCapture :: FigureSpec -> PythonScript
+ Text.Pandoc.Filter.FigureSpec: data FigureSpec
+ Text.Pandoc.Filter.FigureSpec: data SaveFormat
+ Text.Pandoc.Filter.FigureSpec: extension :: SaveFormat -> String
+ Text.Pandoc.Filter.FigureSpec: figurePath :: FigureSpec -> FilePath
+ Text.Pandoc.Filter.FigureSpec: hiresFigurePath :: FigureSpec -> FilePath
+ Text.Pandoc.Filter.FigureSpec: instance Data.Hashable.Class.Hashable Text.Pandoc.Filter.FigureSpec.FigureSpec
+ Text.Pandoc.Filter.FigureSpec: saveFormatFromString :: String -> Maybe SaveFormat
+ Text.Pandoc.Filter.Pyplot: captionKey :: String
+ Text.Pandoc.Filter.Pyplot: directoryKey :: String
+ Text.Pandoc.Filter.Pyplot: dpiKey :: String
+ Text.Pandoc.Filter.Pyplot: includePathKey :: String
+ Text.Pandoc.Filter.Pyplot: saveFormatKey :: String
- Text.Pandoc.Filter.Scripting: type PythonScript = String
+ Text.Pandoc.Filter.Scripting: type PythonScript = Text
Files
- CHANGELOG.md +14/−0
- LICENSE.md +23/−23
- README.md +19/−17
- executable/Main.hs +42/−41
- pandoc-pyplot.cabal +78/−73
- src/Text/Pandoc/Filter/FigureSpec.hs +107/−0
- src/Text/Pandoc/Filter/Pyplot.hs +178/−117
- src/Text/Pandoc/Filter/Scripting.hs +46/−41
- stack.yaml +65/−64
- test/Main.hs +113/−96
CHANGELOG.md view
@@ -2,6 +2,20 @@ pandoc-pyplot uses [Semantic Versioning](http://semver.org/spec/v2.0.0.html) +Release 2.0.0.0+---------------++Many **breaking changes** in this release:++* `pandoc-pyplot` will now determine the filename based on hashing the figure content. Therefore, figures will only be re-generated if necessary.+* Removed the ability to control the filename and format directly using the `plot_target=...` attribute.+* Added the ability to control the directory in which figures will be saved using the `directory=...` attribute.+* Added the possibility to control the figures dots-per-inch (i.e. pixel density) with the `dpi=...` attribute.+* Added the ability to control the figure format with the `format=...` attribute. Possible values are currently `"png"`, `"svg"`, `"pdf"`, and `"jpg"`/`"jpeg"`.+* The confusing `plot_alt=...` attribute has been renamed to `caption=...` for obvious reasons.+* The `plot_include=...` attribute has been renamed to `include=...`.+* Added the generation of a higher resolution figure for every figure `pandoc-pyplot` understands.+ Release 1.1.0.0 ---------------
LICENSE.md view
@@ -1,23 +1,23 @@-[The MIT License (MIT)][]--Copyright (c) 2018 Author name here--Permission is hereby granted, free of charge, to any person obtaining a copy of-this software and associated documentation files (the "Software"), to deal in-the Software without restriction, including without limitation the rights to-use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies-of the Software, and to permit persons to whom the Software is furnished to do-so, subject to the following conditions:--The above copyright notice and this permission notice shall be included in all-copies or substantial portions of the Software.--THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE-SOFTWARE.--[The MIT License (MIT)]: https://opensource.org/licenses/MIT+[The MIT License (MIT)][] + +Copyright (c) 2019 Laurent P. René de Cotret + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +[The MIT License (MIT)]: https://opensource.org/licenses/MIT
README.md view
@@ -8,14 +8,14 @@ ## Usage -The filter recognizes code blocks with the `plot_target` attribute present. It will run the script in the associated code block in a Python interpreter and capture the generated Matplotlib figure. This captured figure will be saved in the located specific by `plot_target`. +The filter recognizes code blocks with the `pyplot` class present. It will run the script in the associated code block in a Python interpreter and capture the generated Matplotlib figure. ### Basic example Here is a basic example using the scripting `matplotlib.pyplot` API: ```markdown - ```{plot_target=my_figure.jpg} + ```{.pyplot} import matplotlib.pyplot as plt plt.figure() @@ -24,10 +24,6 @@ ``` ``` -`pandoc-pyplot` will determine whether the `plot_target` is a relative or absolute path. In case of a relative path (like above), all paths will be considered relative to the current working directory. - -We can control the format of the output file by changing the `plot_target` file extension. All formats supported by Matplotlib on your machine are available. - Putting the above in `input.md`, we can then generate the plot and embed it: ```bash @@ -40,18 +36,20 @@ pandoc --filter pandoc-pyplot input.md --output output.pdf ``` -or any other output format you want. There are more examples in the source repository, in the `\examples` directory. +or any other output format you want. `pandoc-pyplot` is efficient, too: it will detect which figures should be re-generated, and skip the others. +There are more examples in the [source repository](https://github.com/LaurentRDC/pandoc-pyplot), in the `\examples` directory. + ### Link to source code In case of an output format that supports links (e.g. HTML), the embedded image generated by `pandoc-pyplot` will be a link to the source code which was used to generate the file. Therefore, other people can see what Python code was used to create your figures. ### Captions -You can also specify a caption for your image. This is done using the optional `plot_alt` parameter: +You can also specify a caption for your image. This is done using the optional `caption` parameter: ```markdown - ```{plot_target=my_figure.jpg plot_alt="This is a simple figure"} + ```{.pyplot caption="This is a simple figure"} import matplotlib.pyplot as plt plt.figure() @@ -62,7 +60,7 @@ ### Including scripts -If you find yourself always repeating some steps, inclusion of scripts is possible using the `plot_include` parameter. For example, if you want all plots to have the [`ggplot`](https://matplotlib.org/tutorials/introductory/customizing.html#sphx-glr-tutorials-introductory-customizing-py) style, you can write a very short preamble `style.py` like so: +If you find yourself always repeating some steps, inclusion of scripts is possible using the `include` parameter. For example, if you want all plots to have the [`ggplot`](https://matplotlib.org/tutorials/introductory/customizing.html#sphx-glr-tutorials-introductory-customizing-py) style, you can write a very short preamble `style.py` like so: ```python import matplotlib.pyplot as plt @@ -72,7 +70,7 @@ and include it in your document as follows: ```markdown - ```{plot_target=my_figure.jpg plot_include=style.py} + ```{.pyplot include=style.py} plt.figure() plt.plot([0,1,2,3,4], [1,2,3,4,5]) plt.title('This is an example figure') @@ -82,7 +80,7 @@ Which is equivalent to writing the following markdown: ```markdown - ```{plot_target=my_figure.jpg} + ```{.pyplot} import matplotlib.pyplot as plt plt.style.use('ggplot') @@ -92,7 +90,7 @@ ``` ``` -This `plot_include` parameter is perfect for longer documents with many plots. Simply define the style you want in a separate script! You can also import packages this way, or define functions you often use. +This `include` parameter is perfect for longer documents with many plots. Simply define the style you want in a separate script! You can also import packages this way, or define functions you often use. ## Installation @@ -100,6 +98,10 @@ Windows binaries are available on [GitHub](https://github.com/LaurentRDC/pandoc-pyplot/releases). Place the executable in a location that is in your PATH to be able to call it. +### Installers + +Starting with `pandoc-pyplot` version 1.1.0.0, Windows installers are also made available thanks to [Inno Setup](http://www.jrsoftware.org/isinfo.php). + ### From Hackage/Stackage `pandoc-pyplot` is available on Hackage. Using the [`cabal-install`](https://www.haskell.org/cabal/) tool: @@ -121,7 +123,7 @@ Building from source can be done using [`stack`](https://docs.haskellstack.org/en/stable/README/) or [`cabal`](https://www.haskell.org/cabal/): ```bash -git clone github.com/LaurentRDC/pandoc-pyplot.git +git clone https://github.com/LaurentRDC/pandoc-pyplot cd pandoc-pylot stack install # Alternatively, `cabal install` ``` @@ -130,9 +132,9 @@ ### Requirements -This filter only works with the Matplotlib plotting library. Therefore, you a Python interpreter and at least [Matplotlib](https://matplotlib.org/) installed. The python interpreter is expected to be discoverable using the name `"python"` (as opposed to `"python3"`, for example) +This filter only works with the Matplotlib plotting library. Therefore, you a Python interpreter and at least [Matplotlib](https://matplotlib.org/) installed. The Python interpreter is expected to be discoverable using the name `"python"` (as opposed to `"python3"`, for example) -The filter program must be in your `PATH`. In case it is, you can use the filter with Pandoc as follows: +You can use the filter with Pandoc as follows: ```bash pandoc --filter pandoc-pyplot input.md --output output.html @@ -177,4 +179,4 @@ ## Warning -Do not run this filter on unknown documents. There is nothing in `pandoc-pyplot` that can stop a Python script from performing evil actions. This is the reason this package is deemed __unsafe__ in the parlance of [Safe Haskell](https://ghc.haskell.org/trac/ghc/wiki/SafeHaskell). +Do not run this filter on unknown documents. There is nothing in `pandoc-pyplot` that can stop a Python script from performing **evil actions**. This is the reason this package is deemed __unsafe__ in the parlance of [Safe Haskell](https://ghc.haskell.org/trac/ghc/wiki/SafeHaskell).
executable/Main.hs view
@@ -1,41 +1,42 @@-{-# LANGUAGE LambdaCase #-}-module Main where--import System.Environment (getArgs)--import Text.Pandoc.JSON (toJSONFilter)-import Text.Pandoc.Filter.Pyplot (makePlot)--import qualified Data.Version as V-import Paths_pandoc_pyplot (version)---- The formatting is borrowed from Python's argparse library-help :: String-help = "\n\- \\n\- \ usage: pandoc-pyplot [-h, --help] [-v, --version] \n\- \\n\- \ This pandoc filter generates plots from Python code blocks using Matplotlib. \n\- \ This allows to keep documentation and figures up-to-date.\n\- \\n\- \ Optional arguments:\n\- \ -h, --help Show this help message and exit\n\- \ -v, --version Show version number and exit \n\- \\n\- \ To use with pandoc: \n\- \ pandoc --filter pandoc-pyplot input.md --output output.html\n\- \\n\- \ More information can be found in the repository README, located at \n\- \ https://github.com/LaurentRDC/pandoc-pyplot\n"--main :: IO ()-main = do- getArgs >>=- \case- (arg:_) - | arg `elem` ["-h", "--help"] -> showHelp- | arg `elem` ["-v", "--version"] -> showVersion- _ -> toJSONFilter makePlot - where- showHelp = putStrLn help- showVersion = putStrLn (V.showVersion version)+{-# LANGUAGE LambdaCase #-} + +module Main where + +import System.Environment (getArgs) + +import Text.Pandoc.Filter.Pyplot (makePlot) +import Text.Pandoc.JSON (toJSONFilter) + +import qualified Data.Version as V +import Paths_pandoc_pyplot (version) + +-- The formatting is borrowed from Python's argparse library +help :: String +help = + "\n\ + \\n\ + \ usage: pandoc-pyplot [-h, --help] [-v, --version] \n\ + \\n\ + \ This pandoc filter generates plots from Python code blocks using Matplotlib. \n\ + \ This allows to keep documentation and figures in perfect synchronicity.\n\ + \\n\ + \ Optional arguments:\n\ + \ -h, --help Show this help message and exit\n\ + \ -v, --version Show version number and exit \n\ + \\n\ + \ To use with pandoc: \n\ + \ pandoc --filter pandoc-pyplot input.md --output output.html\n\ + \\n\ + \ More information can be found in the repository README, located at \n\ + \ https://github.com/LaurentRDC/pandoc-pyplot\n" + +main :: IO () +main = do + getArgs >>= \case + (arg:_) + | arg `elem` ["-h", "--help"] -> showHelp + | arg `elem` ["-v", "--version"] -> showVersion + _ -> toJSONFilter makePlot + where + showHelp = putStrLn help + showVersion = putStrLn (V.showVersion version)
pandoc-pyplot.cabal view
@@ -1,73 +1,78 @@-name: pandoc-pyplot-version: 1.1.0.0-cabal-version: >= 1.12-synopsis: A Pandoc filter for including figures generated from Matplotlib-description: A pandoc filter for including figures generated from Matplotlib. Keep the document and Python code in the same location. Output from Matplotlib is captured and included as a figure.-category: Documentation-homepage: https://github.com/LaurentRDC/pandoc-pyplot#readme-bug-reports: https://github.com/LaurentRDC/pandoc-pyplot/issues-author: Laurent P. René de Cotret-maintainer: Laurent P. René de Cotret-license: MIT-license-file: LICENSE.md-build-type: Simple-extra-source-files:- CHANGELOG.md- LICENSE.md- README.md- stack.yaml- test/fixtures/*.py--source-repository head- type: git- location: https://github.com/LaurentRDC/pandoc-pyplot--library- exposed-modules:- Text.Pandoc.Filter.Pyplot- Text.Pandoc.Filter.Scripting- other-modules:- Paths_pandoc_pyplot- hs-source-dirs:- src- ghc-options: -Wall -Wcompat- build-depends:- base >=4 && <5- , containers- , directory- , filepath- , pandoc-types >1.12 && <2- , temporary- , typed-process- default-language: Haskell2010--executable pandoc-pyplot- main-is: Main.hs- other-modules:- Paths_pandoc_pyplot- hs-source-dirs:- executable- ghc-options: -Wall -Wcompat -rtsopts -threaded -with-rtsopts=-N- build-depends:- base >=4 && <5- , pandoc-pyplot- , pandoc-types >1.12 && <2- default-language: Haskell2010--test-suite tests- type: exitcode-stdio-1.0- hs-source-dirs: test- main-is: Main.hs- build-depends: base >= 4 && < 5- , directory- , filepath- , pandoc-types >= 1.12 && <= 2- , pandoc-pyplot- , tasty- , tasty-hunit- , tasty-hspec- , temporary- , hspec- , hspec-expectations- default-language: Haskell2010- +name: pandoc-pyplot +version: 2.0.0.0 +cabal-version: >= 1.12 +synopsis: A Pandoc filter for including figures generated from Matplotlib +description: A pandoc filter for including figures generated from Matplotlib. Keep the document and Python code in the same location. Output from Matplotlib is captured and included as a figure. +category: Documentation +homepage: https://github.com/LaurentRDC/pandoc-pyplot#readme +bug-reports: https://github.com/LaurentRDC/pandoc-pyplot/issues +author: Laurent P. René de Cotret +maintainer: Laurent P. René de Cotret +license: MIT +license-file: LICENSE.md +build-type: Simple +extra-source-files: + CHANGELOG.md + LICENSE.md + README.md + stack.yaml + test/fixtures/*.py + +source-repository head + type: git + location: https://github.com/LaurentRDC/pandoc-pyplot + +library + exposed-modules: + Text.Pandoc.Filter.Pyplot + Text.Pandoc.Filter.Scripting + Text.Pandoc.Filter.FigureSpec + other-modules: + Paths_pandoc_pyplot + hs-source-dirs: + src + ghc-options: -Wall -Wcompat + build-depends: + base >=4 && <5 + , containers + , directory + , filepath + , hashable > 1 && < 2 + , pandoc-types >1.12 && <2 + , temporary + , text + , typed-process + , random > 1 && < 2 + default-language: Haskell2010 + +executable pandoc-pyplot + main-is: Main.hs + other-modules: + Paths_pandoc_pyplot + hs-source-dirs: + executable + ghc-options: -Wall -Wcompat -rtsopts -threaded -with-rtsopts=-N + build-depends: + base >=4 && <5 + , pandoc-pyplot + , pandoc-types >1.12 && <2 + default-language: Haskell2010 + +test-suite tests + type: exitcode-stdio-1.0 + hs-source-dirs: test + main-is: Main.hs + build-depends: base >= 4 && < 5 + , directory + , filepath + , hspec + , hspec-expectations + , pandoc-types >= 1.12 && <= 2 + , pandoc-pyplot + , tasty + , tasty-hunit + , tasty-hspec + , temporary + , text + default-language: Haskell2010 +
+ src/Text/Pandoc/Filter/FigureSpec.hs view
@@ -0,0 +1,107 @@+{-# LANGUAGE OverloadedStrings #-} + +{-| +Module : Text.Pandoc.Filter.FigureSpec +Copyright : (c) Laurent P René de Cotret, 2019 +License : MIT +Maintainer : laurent.decotret@outlook.com +Stability : internal +Portability : portable + +This module defines types and functions that help +with keeping track of figure specifications +-} +module Text.Pandoc.Filter.FigureSpec + ( FigureSpec(..) + , SaveFormat(..) + , saveFormatFromString + , figurePath + , hiresFigurePath + , addPlotCapture + -- for testing purposes + , extension + ) where + +import Data.Hashable (Hashable, hash, hashWithSalt) +import qualified Data.Text as T + +import System.FilePath (FilePath, addExtension, + replaceExtension, (</>)) + +import Text.Pandoc.Definition (Attr) +import Text.Pandoc.Filter.Scripting (PythonScript) + +data SaveFormat + = PNG + | PDF + | SVG + | JPG + | EPS + +-- | Parse an image save format string +saveFormatFromString :: String -> Maybe SaveFormat +saveFormatFromString s + | s `elem` ["png", "PNG", ".png"] = Just PNG + | s `elem` ["pdf", "PDF", ".pdf"] = Just PDF + | s `elem` ["svg", "SVG", ".svg"] = Just SVG + | s `elem` ["jpg", "jpeg", "JPG", "JPEG", ".jpg", ".jpeg"] = Just JPG + | s `elem` ["eps", "EPS", ".eps"] = Just EPS + | otherwise = Nothing + +-- | Save format file extension +extension :: SaveFormat -> String +extension PNG = ".png" +extension PDF = ".pdf" +extension SVG = ".svg" +extension JPG = ".jpg" +extension EPS = ".eps" + +-- | Datatype containing all parameters required +-- to run pandoc-pyplot +data FigureSpec = FigureSpec + { caption :: String -- ^ Figure caption. + , script :: PythonScript -- ^ Source code for the figure. + , saveFormat :: SaveFormat -- ^ Save format of the figure + , directory :: FilePath -- ^ Directory where to save the file + , dpi :: Int -- ^ Dots-per-inch of figure + , blockAttrs :: Attr -- ^ Attributes not related to @pandoc-pyplot@ will be propagated. + } + +instance Hashable FigureSpec where + hashWithSalt salt spec = + hashWithSalt salt (caption spec, script spec, directory spec, dpi spec, blockAttrs spec) + +-- | Determine the path a figure should have. +figurePath :: FigureSpec -> FilePath +figurePath spec = (directory spec </> stem spec) + where + stem = flip addExtension ext . show . hash + ext = extension . saveFormat $ spec + +-- | The path to the high-resolution figure. +hiresFigurePath :: FigureSpec -> FilePath +hiresFigurePath spec = flip replaceExtension (".hires" <> ext) . figurePath $ spec + where + ext = extension . saveFormat $ spec + +-- | Modify a Python plotting script to save the figure to a filename. +-- An additional file (with extension PNG) will also be captured. +addPlotCapture :: + FigureSpec -- ^ Path where to save the figure + -> PythonScript -- ^ Code block with added capture +addPlotCapture spec = + mconcat + [ script spec + , "\nimport matplotlib.pyplot as plt" -- Just in case + , plotCapture (figurePath spec) (dpi spec) + , plotCapture (hiresFigurePath spec) (minimum [200, 2 * dpi spec]) + ] + where + plotCapture fname' dpi' = + mconcat + [ "\nplt.savefig(" + , T.pack $ show fname' -- show is required for quotes + , ", dpi=" + , T.pack $ show dpi' + , ")" + ]
src/Text/Pandoc/Filter/Pyplot.hs view
@@ -1,35 +1,43 @@-{-# LANGUAGE MultiWayIf #-} -{-# LANGUAGE Unsafe #-} +{-# LANGUAGE MultiWayIf #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE Unsafe #-} + {-| Module : Text.Pandoc.Filter.Pyplot Description : Pandoc filter to create Matplotlib figures from code blocks -Copyright : (c) Laurent P René de Cotret, 2018 +Copyright : (c) Laurent P René de Cotret, 2019 License : MIT Maintainer : laurent.decotret@outlook.com Stability : stable Portability : portable -This module defines a Pandoc filter @makePlot@ that can be +This module defines a Pandoc filter @makePlot@ that can be used to walk over a Pandoc document and generate figures from Python code blocks. -The syntax for code blocks is simple, Code blocks with the @plot_target=...@ +The syntax for code blocks is simple, Code blocks with the @.pyplot@ attribute will trigger the filter. The code block will be reworked into a Python -script and the output figure will be captured. +script and the output figure will be captured, along with a high-resolution version +of the figure and the source code used to generate the figure. +To trigger pandoc-pyplot, the following is __required__: + + * @.pyplot@: Trigger pandoc-pyplot but let it decide on a filename + Here are the possible attributes what pandoc-pyplot understands: - * @plot_target=...@ (_required_): Filepath where the resulting figure should be saved. - * @plot_alt="..."@ (_optional_): Specify a plot caption (or alternate text). - * @plot_include=...@ (_optional_): Path to a Python script to include before the code block. - Ideal to avoid repetition over many figures. + * @target=...@: Filepath where the resulting figure should be saved. + * @directory=...@ : Directory where to save the figure. + * @caption="..."@: Specify a plot caption (or alternate text). + * @dpi=...@: Specify a value for figure resolution, or dots-per-inch. Default is 80DPI. + * @include=...@: Path to a Python script to include before the code block. Ideal to avoid repetition over many figures. Here are some example blocks in Markdown: @ This is a paragraph -```{plot_target=my_figure.jpg plot_alt="This is a caption."} +```{.pyplot caption="This is a caption."} import matplotlib.pyplot as plt plt.figure() @@ -37,139 +45,192 @@ plt.title('This is an example figure') ``` @ + +This filter was originally designed to be used with [Hakyll](https://jaspervdj.be/hakyll/). +In case you want to use the filter with your own Hakyll setup, you can use a transform +function that works on entire documents: + +@ +import Text.Pandoc.Filter.Pyplot (plotTransform) + +import Hakyll + +-- Unsafe compiler is required because of the interaction +-- in IO (i.e. running an external Python script). +makePlotPandocCompiler :: Compiler (Item String) +makePlotPandocCompiler = + pandocCompilerWithTransformM + defaultHakyllReaderOptions + defaultHakyllWriterOptions + (unsafeCompiler . plotTransform) +@ + -} -module Text.Pandoc.Filter.Pyplot ( - makePlot - , makePlot' -- For testing - , plotTransform - , PandocPyplotError(..) +module Text.Pandoc.Filter.Pyplot + ( makePlot + , plotTransform + , PandocPyplotError(..) + -- For testing purposes only + , makePlot' + , directoryKey + , captionKey + , dpiKey + , includePathKey + , saveFormatKey ) where -import Control.Monad ((>=>)) -import qualified Data.Map.Strict as M -import Data.Maybe (fromMaybe) -import Data.Monoid ((<>)) -import System.Directory (doesDirectoryExist) -import System.FilePath (isValid, replaceExtension, takeDirectory) +import Control.Monad ((>=>)) +import Data.List (intersperse) + +import qualified Data.Map.Strict as Map +import Data.Maybe (fromMaybe) +import Data.Monoid ((<>)) +import qualified Data.Text as T +import qualified Data.Text.IO as T +import Data.Version (showVersion) + +import Paths_pandoc_pyplot (version) + +import System.Directory (createDirectoryIfMissing, + doesFileExist) +import System.FilePath (isValid, makeValid, + replaceExtension, takeDirectory) + import Text.Pandoc.Definition -import Text.Pandoc.Walk (walkM) +import Text.Pandoc.Walk (walkM) -import Text.Pandoc.Filter.Scripting +import Text.Pandoc.Filter.FigureSpec (FigureSpec (..), + SaveFormat (..), addPlotCapture, + figurePath, hiresFigurePath, + saveFormatFromString) +import Text.Pandoc.Filter.Scripting -- | Possible errors returned by the filter -data PandocPyplotError = ScriptError Int -- ^ Running Python script has yielded an error - | InvalidTargetError FilePath -- ^ Invalid figure path - | MissingDirectoryError FilePath -- ^ Directory where to save figure does not exist - | BlockingCallError -- ^ Python script contains a block call to 'show()' - deriving Eq +data PandocPyplotError + = ScriptError Int -- ^ Running Python script has yielded an error + | InvalidTargetError FilePath -- ^ Invalid figure path + | BlockingCallError -- ^ Python script contains a block call to 'show()' + deriving (Eq) +-- | Translate filter error to an error message instance Show PandocPyplotError where - -- | Translate filter error to an error message - show (ScriptError exitcode) = "Script error: plot could not be generated. Exit code " <> (show exitcode) - show (InvalidTargetError fname) = "Target filename " <> fname <> " is not valid." - show (MissingDirectoryError dirname) = "Target directory " <> dirname <> " does not exist." - show BlockingCallError = "Script contains a blocking call to show, like 'plt.show()'" + show (ScriptError exitcode) = + "Script error: plot could not be generated. Exit code " <> (show exitcode) + show (InvalidTargetError fname) = "Target filename " <> fname <> " is not valid." + show BlockingCallError = "Script contains a blocking call to show, like 'plt.show()'" +-- | Keys that pandoc-pyplot will look for in code blocks. These are only exported for testing purposes. +directoryKey, captionKey, dpiKey, includePathKey, saveFormatKey :: String +directoryKey = "directory" --- | Datatype containing all parameters required --- to run pandoc-pyplot -data FigureSpec = FigureSpec - { target :: FilePath -- ^ filepath where generated figure will be saved. - , alt :: String -- ^ Alternate text for the figure (optional). - , script :: PythonScript -- ^ Source code for the figure. - , includePath :: Maybe FilePath -- ^ Path to a Python to be included before the script. - , blockAttrs :: Attr -- ^ Attributes not related to @pandoc-pyplot@ will be propagated. - } +captionKey = "caption" --- | Use figure specification to render a full plot script, including everything except plot capture -renderScript :: FigureSpec -> IO PythonScript -renderScript spec = do - includeScript <- fromMaybe (return "") $ readFile <$> (includePath spec) - return $ mconcat [ "# Source code for ", target spec, "\n" - , "# Generated by pandoc-pyplot\n" - , includeScript, "\n", script spec] +dpiKey = "dpi" --- Keys that pandoc-pyplot will look for in code blocks -targetKey, altTextKey, includePathKey :: String -targetKey = "plot_target" -altTextKey = "plot_alt" -includePathKey = "plot_include" +includePathKey = "include" +saveFormatKey = "format" + +-- | list of all keys related to pandoc-pyplot. +inclusionKeys :: [String] +inclusionKeys = [directoryKey, captionKey, dpiKey, includePathKey, saveFormatKey] + -- | Determine inclusion specifications from Block attributes. -- Note that the target key is required, but all other parameters are optional -parseFigureSpec :: Block -> Maybe FigureSpec -parseFigureSpec (CodeBlock (id', cls, attrs) content) = - createInclusion <$> M.lookup targetKey attrs' - where - attrs' = M.fromList attrs - inclusionKeys = [ targetKey, altTextKey ] - filteredAttrs = filter (\(k,_) -> k `notElem` inclusionKeys) attrs - createInclusion fname = FigureSpec - { target = fname - , alt = M.findWithDefault "Figure generated by pandoc-pyplot" altTextKey attrs' - , script = content - , includePath = M.lookup includePathKey attrs' - -- Propagate attributes that are not related to pandoc-pyplot - , blockAttrs = (id', cls, filteredAttrs) - } -parseFigureSpec _ = Nothing +parseFigureSpec :: Block -> IO (Maybe FigureSpec) +parseFigureSpec (CodeBlock (id', cls, attrs) content) + | "pyplot" `elem` cls = Just <$> figureSpec + | otherwise = return Nothing + where + attrs' = Map.fromList attrs + filteredAttrs = filter (\(k, _) -> k `notElem` inclusionKeys) attrs + dir = makeValid $ Map.findWithDefault "generated" directoryKey attrs' + format = fromMaybe (PNG) $ saveFormatFromString $ Map.findWithDefault "png" saveFormatKey attrs' + includePath = Map.lookup includePathKey attrs' + figureSpec :: IO FigureSpec + figureSpec = do + includeScript <- fromMaybe (return "") $ T.readFile <$> includePath + let header = "# Generated by pandoc-pyplot " <> ((T.pack . showVersion) version) + fullScript = mconcat $ intersperse "\n" [header, includeScript, T.pack content] + caption' = Map.findWithDefault mempty captionKey attrs' + dpi' = read $ Map.findWithDefault "80" dpiKey attrs' + blockAttrs' = (id', filter (/= "pyplot") cls, filteredAttrs) + return $ FigureSpec caption' fullScript format dir dpi' blockAttrs' +parseFigureSpec _ = return Nothing +-- | Check figure specifications for common mistakes +validateSpec :: FigureSpec -> Maybe PandocPyplotError +validateSpec spec + | not (isValid path) = Just $ InvalidTargetError path + | hasBlockingShowCall rendered = Just $ BlockingCallError + | otherwise = Nothing + where + path = figurePath spec + rendered = script spec + +-- | Run the Python script. In case the file already exists, we can safely assume +-- there is no need to re-run it. +runScriptIfNecessary :: FigureSpec -> IO ScriptResult +runScriptIfNecessary spec = do + createDirectoryIfMissing True . takeDirectory $ figurePath spec + fileAlreadyExists <- doesFileExist $ figurePath spec + if fileAlreadyExists + then return ScriptSuccess + else do + result <- runTempPythonScript $ addPlotCapture spec + case result of + ScriptFailure code -> return $ ScriptFailure code + ScriptSuccess + -- Save the original script into a separate file + -- so it can be inspected + -- Note : using a .txt file allows to view source directly + -- in the browser, in the case of HTML output + -> do + let sourcePath = replaceExtension (figurePath spec) ".txt" + T.writeFile sourcePath $ script spec + return ScriptSuccess + -- | Main routine to include Matplotlib plots. --- Code blocks containing the attributes @plot_target@ are considered +-- Code blocks containing the attributes @.pyplot@ are considered -- Python plotting scripts. All other possible blocks are ignored. --- The source code is also saved in another file, which can be access by --- clicking the image makePlot' :: Block -> IO (Either PandocPyplotError Block) -makePlot' block = - case parseFigureSpec block of - -- Could not parse - leave code block unchanged +makePlot' block = do + parsed <- parseFigureSpec block + case parsed of Nothing -> return $ Right block - -- Could parse : run the script and capture output - Just spec -> do - - -- Rendered script, including possible inclusions and other additions - -- except the plot capture. - rendered <- renderScript spec - - let figurePath = target spec - figureDir = takeDirectory figurePath - - -- Check that the directory in which to save the figure exists - validDirectory <- doesDirectoryExist $ takeDirectory figurePath - - if | not (isValid figurePath) -> return $ Left $ InvalidTargetError figurePath - | not validDirectory -> return $ Left $ MissingDirectoryError figureDir - | hasBlockingShowCall rendered -> return $ Left $ BlockingCallError - | otherwise -> do - - -- Running the script - -- A plot capture (plt.savefig(...)) is added as well - result <- runTempPythonScript $ addPlotCapture (target spec) rendered - - case result of - ScriptFailure code -> return $ Left $ ScriptError code - ScriptSuccess -> do - -- Save the original script into a separate file - -- so it can be inspected - -- Note : using a .txt file allows to view source directly - -- in the browser, in the case of HTML output - let sourcePath = replaceExtension figurePath ".txt" - writeFile sourcePath rendered - - -- Propagate attributes that are not related to pandoc-pyplot - let relevantAttrs = blockAttrs spec - srcTarget = Link nullAttr [Str "Source code"] (sourcePath, "") - caption' = [Str $ alt spec, Space, Str "(", srcTarget, Str ")"] + Just spec -> + case validateSpec spec of + Just err -> return $ Left err + Nothing -> do + result <- runScriptIfNecessary spec + case result of + ScriptFailure code -> return $ Left $ ScriptError code + ScriptSuccess -> do + let relevantAttrs = blockAttrs spec + sourcePath = replaceExtension (figurePath spec) ".txt" + hiresPath = hiresFigurePath spec + srcTarget = Link nullAttr [Str "Source code"] (sourcePath, "") + hiresTarget = Link nullAttr [Str "high res."] (hiresPath, "") + -- TODO: use pandoc-types Builder module + caption' = + [ Str $ caption spec + , Space + , Str "(" + , srcTarget + , Str "," + , Space + , hiresTarget + , Str ")" + ] -- To render images as figures with captions, the target title -- must be "fig:" -- Janky? yes - image = Image relevantAttrs caption' (figurePath, "fig:") - - return $ Right $ Para $ [image] + image = Image relevantAttrs caption' (figurePath spec, "fig:") + return $ Right $ Para $ [image] -- | Highest-level function that can be walked over a Pandoc tree. --- All code blocks that have the 'plot_target' parameter will be considered +-- All code blocks that have the '.pyplot' parameter will be considered -- figures. makePlot :: Block -> IO Block makePlot = makePlot' >=> either (fail . show) return
src/Text/Pandoc/Filter/Scripting.hs view
@@ -1,7 +1,9 @@-{-# LANGUAGE Unsafe #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE Unsafe #-} + {-| Module : Text.Pandoc.Filter.Scripting -Copyright : (c) Laurent P René de Cotret, 2018 +Copyright : (c) Laurent P René de Cotret, 2019 License : MIT Maintainer : laurent.decotret@outlook.com Stability : internal @@ -10,59 +12,62 @@ This module defines types and functions that help with running Python scripts. -} - -module Text.Pandoc.Filter.Scripting ( - runTempPythonScript - , addPlotCapture +module Text.Pandoc.Filter.Scripting + ( runTempPythonScript , hasBlockingShowCall , PythonScript , ScriptResult(..) -) where + ) where -import System.Exit (ExitCode(..)) -import System.FilePath ((</>)) -import System.IO.Temp (getCanonicalTemporaryDirectory) -import System.Process.Typed (runProcess, shell) +import Data.Text (Text) +import qualified Data.Text as T +import qualified Data.Text.IO as T -import Data.Monoid (Any(..), (<>)) +import Data.Hashable (hash) +import System.Exit (ExitCode (..)) +import System.FilePath ((</>)) +import System.IO.Temp (getCanonicalTemporaryDirectory) +import System.Process.Typed (runProcess, shell) + +import Data.Monoid (Any (..), (<>)) + -- | String representation of a Python script -type PythonScript = String +type PythonScript = Text -- | Possible result of running a Python script -data ScriptResult = ScriptSuccess - | ScriptFailure Int +data ScriptResult + = ScriptSuccess + | ScriptFailure Int -- | Take a python script in string form, write it in a temporary directory, --- then execute it. -runTempPythonScript :: PythonScript -- ^ Content of the script - -> IO ScriptResult -- ^ Result with exit code. -runTempPythonScript script = do - -- Write script to temporary directory - scriptPath <- (</> "pandoc-pyplot.py") <$> getCanonicalTemporaryDirectory - writeFile scriptPath script - -- Execute script - ec <- runProcess $ shell $ "python " <> (show scriptPath) - case ec of - ExitSuccess -> return ScriptSuccess - ExitFailure code -> return $ ScriptFailure code - --- | Modify a Python plotting script to save the figure to a filename. -addPlotCapture :: FilePath -- ^ Path where to save the figure - -> PythonScript -- ^ Raw code block - -> PythonScript -- ^ Code block with added capture -addPlotCapture fname content = - mconcat [ content - , "\nimport matplotlib.pyplot as plt" -- Just in case - , "\nplt.savefig(" <> show fname <> ")\n\n" - ] +-- then execute it. +runTempPythonScript :: + PythonScript -- ^ Content of the script + -> IO ScriptResult -- ^ Result with exit code. +runTempPythonScript script + -- Write script to temporary directory + -- We involve the script hash as a temporary filename + -- so that there is never any collision + = do + scriptPath <- (</> hashedPath) <$> getCanonicalTemporaryDirectory + T.writeFile scriptPath script + -- Execute script + ec <- runProcess $ shell $ "python " <> (show scriptPath) + case ec of + ExitSuccess -> return ScriptSuccess + ExitFailure code -> return $ ScriptFailure code + where + hashedPath = show . hash $ script -- | Detect the presence of a blocking show call, for example "plt.show()" hasBlockingShowCall :: PythonScript -> Bool -hasBlockingShowCall script = anyOf +hasBlockingShowCall script = + anyOf [ "plt.show()" `elem` scriptLines + , "pyplot.show()" `elem` scriptLines , "matplotlib.pyplot.show()" `elem` scriptLines ] - where - scriptLines = lines script - anyOf xs = getAny $ mconcat $ Any <$> xs + where + scriptLines = T.lines script + anyOf xs = getAny $ mconcat $ Any <$> xs
stack.yaml view
@@ -1,64 +1,65 @@-# This file was automatically generated by 'stack init'-#-# Some commonly used options have been documented as comments in this file.-# For advanced use and comprehensive documentation of the format, please see:-# https://docs.haskellstack.org/en/stable/yaml_configuration/--# Resolver to choose a 'specific' stackage snapshot or a compiler version.-# A snapshot resolver dictates the compiler version and the set of packages-# to be used for project dependencies. For example:-#-# resolver: lts-3.5-# resolver: nightly-2015-09-21-# resolver: ghc-7.10.2-#-# The location of a snapshot can be provided as a file or url. Stack assumes-# a snapshot provided as a file might change, whereas a url resource does not.-#-# resolver: ./custom-snapshot.yaml-# resolver: https://example.com/snapshots/2018-01-01.yaml-resolver: lts-12.10--# User packages to be built.-# Various formats can be used as shown in the example below.-#-# packages:-# - some-directory-# - https://example.com/foo/bar/baz-0.0.2.tar.gz-# - location:-# git: https://github.com/commercialhaskell/stack.git-# commit: e7b331f14bcffb8367cd58fbfc8b40ec7642100a-# - location: https://github.com/commercialhaskell/stack/commit/e7b331f14bcffb8367cd58fbfc8b40ec7642100a-# subdirs:-# - auto-update-# - wai-packages:-- .-# Dependency packages to be pulled from upstream that are not in the resolver-# using the same syntax as the packages field.-# (e.g., acme-missiles-0.3)-# extra-deps: []--# Override default flag values for local packages and extra-deps-# flags: {}--# Extra package databases containing global packages-# extra-package-dbs: []--# Control whether we use the GHC we find on the path-# system-ghc: true-#-# Require a specific version of stack, using version ranges-# require-stack-version: -any # Default-# require-stack-version: ">=1.10"-#-# Override the architecture used by stack, especially useful on Windows-# arch: i386-# arch: x86_64-#-# Extra directories used by stack for building-# extra-include-dirs: [/path/to/dir]-# extra-lib-dirs: [/path/to/dir]-#-# Allow a newer minor version of GHC than the snapshot specifies-# compiler-check: newer-minor+# This file was automatically generated by 'stack init' +# +# Some commonly used options have been documented as comments in this file. +# For advanced use and comprehensive documentation of the format, please see: +# https://docs.haskellstack.org/en/stable/yaml_configuration/ + +# Resolver to choose a 'specific' stackage snapshot or a compiler version. +# A snapshot resolver dictates the compiler version and the set of packages +# to be used for project dependencies. For example: +# +# resolver: lts-3.5 +# resolver: nightly-2015-09-21 +# resolver: ghc-7.10.2 +# +# The location of a snapshot can be provided as a file or url. Stack assumes +# a snapshot provided as a file might change, whereas a url resource does not. +# +# resolver: ./custom-snapshot.yaml +# resolver: https://example.com/snapshots/2018-01-01.yaml +resolver: lts-13.14 + +# User packages to be built. +# Various formats can be used as shown in the example below. +# +# packages: +# - some-directory +# - https://example.com/foo/bar/baz-0.0.2.tar.gz +# - location: +# git: https://github.com/commercialhaskell/stack.git +# commit: e7b331f14bcffb8367cd58fbfc8b40ec7642100a +# - location: https://github.com/commercialhaskell/stack/commit/e7b331f14bcffb8367cd58fbfc8b40ec7642100a +# subdirs: +# - auto-update +# - wai +packages: +- . +# Dependency packages to be pulled from upstream that are not in the resolver +# using the same syntax as the packages field. +# (e.g., acme-missiles-0.3) +extra-deps: +- hindent-5.2.7@sha256:a0a90f7cc160eeba3816e53f310e16942e68d9a09fe092d9835ea6bbbcd2cd91 + +# Override default flag values for local packages and extra-deps +# flags: {} + +# Extra package databases containing global packages +# extra-package-dbs: [] + +# Control whether we use the GHC we find on the path +# system-ghc: true +# +# Require a specific version of stack, using version ranges +# require-stack-version: -any # Default +# require-stack-version: ">=1.10" +# +# Override the architecture used by stack, especially useful on Windows +# arch: i386 +# arch: x86_64 +# +# Extra directories used by stack for building +# extra-include-dirs: [/path/to/dir] +# extra-lib-dirs: [/path/to/dir] +# +# Allow a newer minor version of GHC than the snapshot specifies +# compiler-check: newer-minor
test/Main.hs view
@@ -1,126 +1,143 @@+{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE OverloadedStrings #-} module Main where -import Control.Monad (unless) -import Data.List (isInfixOf) +import Control.Monad (unless) -import Test.Tasty -import Test.Tasty.HUnit +import Data.List (isInfixOf) +import Data.Text (unpack) -import qualified Text.Pandoc.Filter.Pyplot as Filter -import qualified Text.Pandoc.Filter.Scripting as Filter -import Text.Pandoc.JSON +import Test.Tasty +import Test.Tasty.HUnit -import System.Directory (doesFileExist) -import System.FilePath ((</>)) -import System.IO.Temp (getCanonicalTemporaryDirectory) +import qualified Text.Pandoc.Filter.FigureSpec as P +import qualified Text.Pandoc.Filter.Pyplot as P +import qualified Text.Pandoc.Filter.Scripting as P +import Text.Pandoc.JSON +import System.Directory (createDirectory, + createDirectoryIfMissing, + doesDirectoryExist, + doesFileExist, listDirectory, + removeDirectoryRecursive, + removePathForcibly) +import System.FilePath (isExtensionOf, (</>)) +import System.IO.Temp (getCanonicalTemporaryDirectory) + main :: IO () -main = defaultMain $ - testGroup "Text.Pandoc.Filter.Pyplot" - [ testFileCreation - , testFileInclusion - , testBlockingCallError - ] +main = + defaultMain $ + testGroup + "Text.Pandoc.Filter.Pyplot" + [testFileCreation, testFileInclusion, testSaveFormat, testBlockingCallError] --- | Create a code block with the right attributes to trigger pandoc-pyplot -mkPlotCodeBlock :: FilePath -- ^ Plot target - -> String -- ^ Plot alt description - -> String -- ^ Plot caption - -> Maybe FilePath -- ^ Possible inclusion - -> Filter.PythonScript -- ^ Script - -> Block -mkPlotCodeBlock target alt caption include script = CodeBlock attrs script - where - attrs = case include of - Nothing -> ( mempty , mempty - , [ ("plot_target", target) - , ("plot_alt", alt) - , ("plot_caption", caption) - ] - ) - Just includePath -> ( mempty , mempty - , [ ("plot_target", target) - , ("plot_alt", alt) - , ("plot_caption", caption) - , ("plot_include", includePath) - ] - ) +plotCodeBlock :: P.PythonScript -> Block +plotCodeBlock script = CodeBlock (mempty, ["pyplot"], mempty) (unpack script) +addCaption :: String -> Block -> Block +addCaption caption (CodeBlock (id', cls, attrs) script) = + CodeBlock (id', cls, attrs ++ [(P.captionKey, caption)]) script + +addDirectory :: FilePath -> Block -> Block +addDirectory dir (CodeBlock (id', cls, attrs) script) = + CodeBlock (id', cls, attrs ++ [(P.directoryKey, dir)]) script + +addInclusion :: FilePath -> Block -> Block +addInclusion inclusionPath (CodeBlock (id', cls, attrs) script) = + CodeBlock (id', cls, attrs ++ [(P.includePathKey, inclusionPath)]) script + +addSaveFormat :: P.SaveFormat -> Block -> Block +addSaveFormat saveFormat (CodeBlock (id', cls, attrs) script) = + CodeBlock (id', cls, attrs ++ [(P.saveFormatKey, P.extension saveFormat)]) script + +addDPI :: Int -> Block -> Block +addDPI dpi (CodeBlock (id', cls, attrs) script) = + CodeBlock (id', cls, attrs ++ [(P.dpiKey, show dpi)]) script + -- | Assert that a file exists -assertFileExists :: HasCallStack - => FilePath - -> Assertion +assertFileExists :: HasCallStack => FilePath -> Assertion assertFileExists filepath = do - fileExists <- doesFileExist filepath + fileExists <- doesFileExist filepath unless fileExists (assertFailure msg) - where - msg = mconcat ["File ", filepath, " does not exist."] + where + msg = mconcat ["File ", filepath, " does not exist."] --- | Assert that a list first list is contained, +-- | Assert that the first list is contained, -- wholly and intact, anywhere within the second. -assertIsInfix :: (Eq a, Show a, HasCallStack) - => [a] - -> [a] - -> Assertion -assertIsInfix xs ys = - unless (xs `isInfixOf` ys) (assertFailure msg) - where - msg = mconcat ["Expected ", show xs, " to be an infix of ", show ys] +assertIsInfix :: (Eq a, Show a, HasCallStack) => [a] -> [a] -> Assertion +assertIsInfix xs ys = unless (xs `isInfixOf` ys) (assertFailure msg) + where + msg = mconcat ["Expected ", show xs, " to be an infix of ", show ys] +-- Ensure a directory is empty but exists. +ensureDirectoryExistsAndEmpty :: FilePath -> IO () +ensureDirectoryExistsAndEmpty dir = do + exists <- doesDirectoryExist dir + if exists + then removePathForcibly dir + else return () + createDirectory dir + ------------------------------------------------------------------------------- -- Test that plot files and source files are created when the filter is run testFileCreation :: TestTree -testFileCreation = testCase "writes output and source files" $ do - tempDir <- getCanonicalTemporaryDirectory - let codeBlock = mkPlotCodeBlock - (tempDir </> "test.png") - mempty - mempty - Nothing - "import matplotlib.pyplot as plt\n" - _ <- Filter.makePlot' codeBlock - assertFileExists (tempDir </> "test.png") - assertFileExists (tempDir </> "test.txt") +testFileCreation = + testCase "writes output files in appropriate directory" $ do + tempDir <- (</> "test-file-creation") <$> getCanonicalTemporaryDirectory + ensureDirectoryExistsAndEmpty tempDir + let codeBlock = (addDirectory tempDir $ plotCodeBlock "import matplotlib.pyplot as plt\n") + _ <- P.makePlot' codeBlock + filesCreated <- length <$> listDirectory tempDir + assertEqual "" filesCreated 3 ------------------------------------------------------------------------------- -- Test that included files are found within the source testFileInclusion :: TestTree -testFileInclusion = testCase "includes plot inclusions" $ do - tempDir <- getCanonicalTemporaryDirectory - - let codeBlock = mkPlotCodeBlock - (tempDir </> "test.png") - mempty - mempty - (Just "test/fixtures/include.py") - "import matplotlib.pyplot as plt\n" - _ <- Filter.makePlot' codeBlock +testFileInclusion = + testCase "includes plot inclusions" $ do + tempDir <- (</> "test-file-inclusion") <$> getCanonicalTemporaryDirectory + ensureDirectoryExistsAndEmpty tempDir + let codeBlock = + (addInclusion "test/fixtures/include.py" $ + addDirectory tempDir $ plotCodeBlock "import matplotlib.pyplot as plt\n") + _ <- P.makePlot' codeBlock + inclusion <- readFile "test/fixtures/include.py" + sourcePath <- head . filter (isExtensionOf "txt") <$> listDirectory tempDir + src <- readFile (tempDir </> sourcePath) + assertIsInfix inclusion src - inclusion <- readFile "test/fixtures/include.py" - src <- readFile (tempDir </> "test.txt") - assertIsInfix inclusion src +------------------------------------------------------------------------------- +-- Test that the files are saved in the appropriate format +testSaveFormat :: TestTree +testSaveFormat = + testCase "saves in the appropriate format" $ do + tempDir <- (</> "test-safe-format") <$> getCanonicalTemporaryDirectory + ensureDirectoryExistsAndEmpty tempDir + let codeBlock = + (addSaveFormat P.JPG $ + addDirectory tempDir $ + plotCodeBlock + "import matplotlib.pyplot as plt\nplt.figure()\nplt.plot([1,2], [1,2])") + _ <- P.makePlot' codeBlock + numberjpgFiles <- + length <$> filter (isExtensionOf (P.extension P.JPG)) <$> + listDirectory tempDir + assertEqual "" numberjpgFiles 2 ------------------------------------------------------------------------------- -- Test that a script containing a blockign call to matplotlib.pyplot.show -- returns the appropriate error testBlockingCallError :: TestTree -testBlockingCallError = testCase "raises an exception for blocking calls" $ do - tempDir <- getCanonicalTemporaryDirectory - - let codeBlock = mkPlotCodeBlock - (tempDir </> "test.png") - mempty - mempty - Nothing - "import matplotlib.pyplot as plt\nplt.show()" - - result <- Filter.makePlot' codeBlock - case result of - Right block -> assertFailure "did not catch the expected blocking call" - Left error -> - if error == Filter.BlockingCallError - then pure () - else assertFailure "did not catch the expected blocking call" - +testBlockingCallError = + testCase "raises an exception for blocking calls" $ do + tempDir <- getCanonicalTemporaryDirectory + let codeBlock = plotCodeBlock "import matplotlib.pyplot as plt\nplt.show()" + result <- P.makePlot' codeBlock + case result of + Right block -> assertFailure "did not catch the expected blocking call" + Left error -> + if error == P.BlockingCallError + then pure () + else assertFailure "did not catch the expected blocking call" -------------------------------------------------------------------------------