packages feed

PyF 0.7.3.0 → 0.8.0.0

raw patch · 11 files changed

+413/−251 lines, 11 filesdep +bytestringdep ~basedep ~textPVP ok

version bump matches the API change (PVP)

Dependencies added: bytestring

Dependency ranges changed: base, text

API changes (from Hackage documentation)

- PyF: f :: QuasiQuoter
- PyF: fWithDelimiters :: (Char, Char) -> QuasiQuoter
- PyF: instance PyF.MagicFormat (GHC.Types.IO ())
- PyF: instance PyF.MagicFormat Data.Text.Internal.Builder.Builder
- PyF: instance PyF.MagicFormat Data.Text.Internal.Lazy.Text
- PyF: instance PyF.MagicFormat Data.Text.Internal.Text
- PyF: instance PyF.MagicFormat [GHC.Types.Char]
- PyF.Internal.QQ: instance (TypeError ...) => PyF.Internal.QQ.Categorise PyF.Internal.QQ.DisableForString Data.Text.Internal.Lazy.Text
- PyF.Internal.QQ: instance (TypeError ...) => PyF.Internal.QQ.Categorise PyF.Internal.QQ.DisableForString Data.Text.Internal.Text
- PyF.Internal.QQ: instance (TypeError ...) => PyF.Internal.QQ.Categorise PyF.Internal.QQ.DisableForString GHC.Base.String
- PyF.Internal.QQ: instance PyF.Internal.QQ.Categorise PyF.Internal.QQ.EnableForString Data.Text.Internal.Lazy.Text
- PyF.Internal.QQ: instance PyF.Internal.QQ.Categorise PyF.Internal.QQ.EnableForString Data.Text.Internal.Text
- PyF.Internal.QQ: instance PyF.Internal.QQ.Categorise PyF.Internal.QQ.EnableForString GHC.Base.String
- PyF.Internal.QQ: instance PyF.Internal.QQ.Categorise k GHC.Int.Int16
- PyF.Internal.QQ: instance PyF.Internal.QQ.Categorise k GHC.Int.Int32
- PyF.Internal.QQ: instance PyF.Internal.QQ.Categorise k GHC.Int.Int64
- PyF.Internal.QQ: instance PyF.Internal.QQ.Categorise k GHC.Int.Int8
- PyF.Internal.QQ: instance PyF.Internal.QQ.Categorise k GHC.Integer.Type.Integer
- PyF.Internal.QQ: instance PyF.Internal.QQ.Categorise k GHC.Natural.Natural
- PyF.Internal.QQ: instance PyF.Internal.QQ.Categorise k GHC.Types.Double
- PyF.Internal.QQ: instance PyF.Internal.QQ.Categorise k GHC.Types.Float
- PyF.Internal.QQ: instance PyF.Internal.QQ.Categorise k GHC.Types.Int
- PyF.Internal.QQ: instance PyF.Internal.QQ.Categorise k GHC.Types.Word
- PyF.Internal.QQ: instance PyF.Internal.QQ.Categorise k GHC.Word.Word16
- PyF.Internal.QQ: instance PyF.Internal.QQ.Categorise k GHC.Word.Word32
- PyF.Internal.QQ: instance PyF.Internal.QQ.Categorise k GHC.Word.Word64
- PyF.Internal.QQ: instance PyF.Internal.QQ.Categorise k GHC.Word.Word8
+ PyF: fmt :: QuasiQuoter
+ PyF: fmtWithDelimiters :: (Char, Char) -> QuasiQuoter
+ PyF.Class: PyFFractional :: PyFCategory
+ PyF.Class: PyFIntegral :: PyFCategory
+ PyF.Class: PyFString :: PyFCategory
+ PyF.Class: class PyFToString t
+ PyF.Class: data PyFCategory
+ PyF.Class: instance PyF.Class.PyFToString Data.Text.Internal.Lazy.Text
+ PyF.Class: instance PyF.Class.PyFToString Data.Text.Internal.Text
+ PyF.Class: instance PyF.Class.PyFToString GHC.Base.String
+ PyF.Class: pyfToString :: (PyFToString t, Show t) => t -> String
+ PyF.Class: type family PyFClassify t :: PyFCategory
+ PyF.Internal.PythonSyntax: type Parser t = Parsec Void String t
+ PyF.Internal.QQ: instance (GHC.Show.Show t, GHC.Real.Integral t) => PyF.Internal.QQ.FormatAny2 'PyF.Class.PyFIntegral t k
+ PyF.Internal.QQ: instance (TypeError ...) => PyF.Internal.QQ.FormatAny2 'PyF.Class.PyFString t 'PyF.Formatters.AlignNumber
+ PyF.Internal.QQ: instance GHC.Float.RealFloat t => PyF.Internal.QQ.FormatAny2 'PyF.Class.PyFFractional t k
+ PyF.Internal.QQ: instance PyF.Class.PyFToString t => PyF.Internal.QQ.FormatAny2 'PyF.Class.PyFString t 'PyF.Formatters.AlignAll
+ PyF.Internal.QQ: instance PyF.Internal.QQ.FormatAny2 (PyF.Class.PyFClassify t) t k => PyF.Internal.QQ.FormatAny t k

Files

ChangeLog.md view
@@ -1,5 +1,20 @@ # Revision history for PyF +## 0.8.0.0 -- 2019-08-06++- `f` (and `fWithDelimiters`) were renamed `fmt` (`fmtWithDelimiters`). `f` was causing too much shadowing in any codebase.+- PyF now exposes the typeclass `PyFToString` and `PyFClassify` which can be extended to support any type as input for the formatters.+- PyF now uses `Data.String.IsString t` as its output type if `OverloadedString` is enabled. It means that it behaves as a real haskell string literal.+- A caveat of the previous change is that PyF does not have instances for `IO` anymore.++### bugfixes and general improvements++- An important amount of bugfixs+- Error reporting for generic formatting (i.e. formatting without a specified type) is now more robust+- Template haskell splices are simpler. This leads to more efficient / small generated code and in the event of this code appears in a GHC error message, it is more readable.+- PyF now longer emit unnecessary default typing.++ ## 0.7.3.0 -- 2019-02-28  - Tests: fix non reproducible tests
PyF.cabal view
@@ -1,5 +1,5 @@ name:                PyF-version:             0.7.3.0+version:             0.8.0.0 synopsis: Quasiquotations for a python like interpolated string formater description: Quasiquotations for a python like interpolated string formater. license:             BSD3@@ -14,6 +14,7 @@ library   exposed-modules:                   PyF+                  PyF.Class                   PyF.Internal.PythonSyntax                   PyF.Internal.QQ                   PyF.Internal.Extensions@@ -42,6 +43,14 @@   other-modules: SpecUtils SpecCustomDelimiters   build-tools:  python3   build-depends:       base, PyF, hspec, text, template-haskell, process+  ghc-options:         -Wall -threaded -rtsopts -with-rtsopts=-N+  default-language:    Haskell2010++test-suite pyf-overloaded+  type:                exitcode-stdio-1.0+  hs-source-dirs:      test+  main-is:             SpecOverloaded.hs+  build-depends:       base, PyF, hspec, text, bytestring   ghc-options:         -Wall -threaded -rtsopts -with-rtsopts=-N   default-language:    Haskell2010 
Readme.md view
@@ -14,7 +14,7 @@ >>> name = "Dave" >>> age = 54 ->>> [f|Person's name is {name}, age is {age:x}|]+>>> [fmt|Person's name is {name}, age is {age:x}|] "Person's name is Dave, age is 36" ``` @@ -39,25 +39,25 @@  ```haskell >>> name = "Guillaume"->>> [f|{name:<11}|]+>>> [fmt|{name:<11}|] "Guillaume  "->>> [f|{name:>11}|]+>>> [fmt|{name:>11}|] "  Guillaume"->>> [f|{name:|^13}|]+>>> [fmt|{name:|^13}|] "||Guillaume||" ```  Padding inside `=` the sign:  ```haskell->>> [f|{-pi:=10.3}|]+>>> [fmt|{-pi:=10.3}|] "-    3.142" ```  ## Float rounding  ```haskell->>> [f|{pi:.2}|]+>>> [fmt|{pi:.2}|] "3.14" ``` @@ -65,11 +65,11 @@  ```haskell >>> v = 31->>> [f|Binary: {v:#b}|]+>>> [fmt|Binary: {v:#b}|] "Binary: 0b11111"->>> [f|Octal (no prefix): {age:o}|]+>>> [fmt|Octal (no prefix): {age:o}|] "Octal (no prefix): 37"->>> [f|Hexa (caps and prefix): {age:#X}|]+>>> [fmt|Hexa (caps and prefix): {age:#X}|] "Hexa (caps and prefix): 0x1F" ``` @@ -78,9 +78,9 @@ Using `,` or `_`.  ```haskell->>> [f|{10 ^ 9 - 1:,}|]+>>> [fmt|{10 ^ 9 - 1:,}|] "999,999,999"->>> [f|{2 ^ 32  -1:_b}|]+>>> [fmt|{2 ^ 32  -1:_b}|] "1111_1111_1111_1111_1111_1111_1111_1111" ``` @@ -89,9 +89,9 @@ Using `+` to display the positive sign (if any) or ` ` to display a space instead:  ```haskell->>> [f|{pi:+.3}|]+>>> [fmt|{pi:+.3}|] "+3.142"->>> [f|{pi: .3}|]+>>> [fmt|{pi: .3}|] " 3.142" ``` @@ -109,9 +109,9 @@ First argument inside the curly braces can be a valid Haskell expression, for example:  ```haskell->>> [f|2pi = {2* pi:.2}|]+>>> [fmt|2pi = {2* pi:.2}|] 6.28->>> [f|tail "hello" = {tail "hello":->6}|]+>>> [fmt|tail "hello" = {tail "hello":->6}|] "tail \"hello\" = --ello" ``` @@ -122,7 +122,7 @@ Most options can be combined. This generally leads to totally unreadable format string ;)  ```haskell->>> [f|{pi:~>5.2}|]+>>> [fmt|{pi:~>5.2}|] "~~3.14" ``` @@ -131,7 +131,7 @@ You can ignore a line break with `\` if needed. For example:  ```haskell-[f|\+[fmt|\ - a - b\ |]@@ -141,18 +141,25 @@  # Output type -*PyF* main entry point `f` is polymorphic and can represents `Text`, lazy `Text`, `String`, lazy text `Builder` or `IO` operations. Most of the time, type inference will do the right thing for you, but you may need to add type annotations.--For example:+*PyF* aims at extending the string literal syntax. As such, it default to `String` type. However, if the `OverloadedString` is enabled, PyF will happilly generate `IsString t => t` instead. This means that you can use PyF to generate `String`, but also `Text` and why not `ByteString`, with all the caveats known to this extension.  ```haskell->>> [f|hello {pi.2}|] :: String+>>> [fmt|hello {pi.2}|] :: String "hello 3.14" ``` -Note: it works in ghci without any type annotation if the extensions-`OverloadedStrings` and `ExtendedDefaultRules` are enabled.+# Custom types +PyF can format three categories of input types:++- Floating. Using the `f`, `g`, `e`, ... type specifiers. Any type instance of `RealFloat` can be formated as such.+- Integral. Using the `d`, `b`, `x`, `o`, ... type specifiers. Any type instance of `Integral` can be formated as such.+- String. Using the `s` type specifier. Any type instance of `PyFToString` can be formated as such.++See `PyF.Class` if you want to create new instances for the `PyFToString` class.++By default, if you do not provide any type specifier, PyF uses the `PyFClassify` type class to decide if your type must be formated as a Floating, Integral or String.+ # Caveats  ## Type inference@@ -161,7 +168,7 @@  ```haskell >>> v = 10 :: Double->>> [f|A float: {v}|]+>>> [fmt|A float: {v}|] A float: 10 ``` @@ -176,7 +183,7 @@ - Any parsing error on the mini language results in a clear indication of the error, for example:  ```haskell->>> [f|{age:.3d}|]+>>> [fmt|{age:.3d}|]  <interactive>:77:4: error:     • <interactive>:1:8:@@ -189,7 +196,7 @@ - Error in variable name are also readable:  ```haskell->>> [f|{toto}|]+>>> [fmt|{toto}|] <interactive>:78:4: error: Variable not in scope: toto ``` @@ -197,7 +204,7 @@   too polymorphic), you will get an awful error:  ```haskell->>*> [f|{True:d}|]+>>*> [fmt|{True:d}|]  <interactive>:80:10: error:     • No instance for (Integral Bool)@@ -205,24 +212,10 @@ ... ``` -- There is also one class of error related to alignement which can be triggered, when using alignement inside sign (i.e. `=`) with string. This can fail in two flavors:--```haskell->>> [f|{"hello":=10s}|]--<interactive>:88:1: error:-    • Exception when trying to run compile-time code:-        String Cannot be aligned with the inside `=` mode-CallStack (from HasCallStack):-  error, called at src/PyF/Internal/QQ.hs:143:18 in PyF-0.4.0.0-inplace:PyF.Internal.QQ-      Code: quoteExp f "{\"hello\":=10s}"-    • In the quasi-quotation: [f|{"hello":=10s}|]-```--And+- There is also one class of error related to alignement which can be triggered, when using alignement inside sign (i.e. `=`) with string:  ```haskell-*PyF PyF.Internal.QQ> [f|{"hello":=10}|]+*PyF PyF.Internal.QQ> [fmt|{"hello":=10}|]  <interactive>:89:10: error:     • String Cannot be aligned with the inside `=` mode@@ -232,7 +225,7 @@ - Finally, if you make any type error inside the expression field, you are on your own:  ```haskell->>> [f|{3 + pi + "hello":10}|]+>>> [fmt|{3 + pi + "hello":10}|]  <interactive>:99:10: error:     • No instance for (Floating [Char]) arising from a use of ‘pi’@@ -253,7 +246,7 @@ import PyF  myCustomFormatter :: QuasiQuoter-myCustomFormatter = fWithDelimiters ('@','!')+myCustomFormatter = fmtWithDelimiters ('@','!') ```  Later, in another module:@@ -276,8 +269,8 @@  - Number `n` formatter is not supported. In python this formatter can format a number and use current locale information for decimal part and thousand separator. There is no plan to support that because of the impure interface needed to read the locale. - Python support sub variables in the formatting options, such as `{varname:.{precision}}`, we should too. However should we accept `String` parameter (such as `<`), with a possible runtime error, or should we use the `ADT` such as `AlignRight`?-- Python literal integers accepts binary/octal/hexa/decimal literals, PyF only accept decimal ones, hdece in to plan to support that, if you really need to format a float with a number of digit provided as a binary constant, open an issue.-- Python support adding custom formatters for new types, such as date. This may be really cool, for example `[f|{today:%Y-%M-%D}`. I don't know how to support that now.+- Python literal integers accepts binary/octal/hexa/decimal literals, PyF only accept decimal ones, I don't have a plan to support that, if you really need to format a float with a number of digit provided as a binary constant, open an issue.+- Python support adding custom formatters for new types, such as date. This may be really cool, for example `[fmt|{today:%Y-%M-%D}`. I don't know how to support that now.  ### Difference @@ -295,15 +288,6 @@ cabal new-test ``` -# TODO--- Improve the error reporting with more Parsec annotation-- Improve the parser for sub-expression (handle the `:` and `}` cases if possible).-- Allow extension to others type / custom formatters (for date for example)-- Improve code quality. This code is really ugly, but there is a really strong test suite so, well.-- Work on performance, do we really care? For now, everything is internally done with `String`.-- Directly expose the formatter to be used as a template haskell splice- # Library note  `PyF.Formatters` exposes two functions to format numbers. They are type-safe (as much as possible) and comes with a combination of formatting options not seen in other formatting libraries:@@ -316,4 +300,3 @@ # Conclusion  Don't hesitate to make any suggestion, I'll be more than happy to work on it.-
src/PyF.hs view
@@ -4,25 +4,20 @@ {- | A lot of quasiquoters to format and interpolate string expression -} module PyF-  ( f+  ( fmt    -- * With custom delimiters-  , fWithDelimiters+  , fmtWithDelimiters+  , module PyF.Class   ) where  import           Language.Haskell.TH.Quote (QuasiQuoter(..))-import qualified PyF.Internal.QQ as QQ--import           Language.Haskell.TH--import qualified Data.Text.Lazy as LText-import qualified Data.Text.Lazy.IO as LText-import qualified Data.Text as SText-import qualified Data.Text.Lazy.Builder as Builder+import PyF.Internal.QQ (toExp)+import PyF.Class  templateF :: (Char, Char) -> String -> QuasiQuoter templateF delimiters fName = QuasiQuoter {-    quoteExp = \s -> (AppE (VarE 'magicFormat)) <$> (QQ.toExp delimiters s)+    quoteExp = \s -> (toExp delimiters s)   , quotePat = err "pattern"   , quoteType = err "type"   , quoteDec = err "declaration"@@ -30,30 +25,13 @@   where     err name = error (fName ++ ": This QuasiQuoter can not be used as a " ++ name ++ "!") --- | Generic formatter, can format an expression to (lazy) Text, String, Builder and IO () depending on type inference-f :: QuasiQuoter-f = templateF pythonDelimiters "f"--fWithDelimiters :: (Char, Char) -> QuasiQuoter-fWithDelimiters delimiters = templateF delimiters "fWithDelimiters"--class MagicFormat t where-  magicFormat :: Builder.Builder -> t--instance MagicFormat (IO ()) where-  magicFormat = LText.putStrLn . Builder.toLazyText--instance MagicFormat [Char] where-  magicFormat = LText.unpack . Builder.toLazyText--instance MagicFormat SText.Text where-  magicFormat = LText.toStrict . Builder.toLazyText--instance MagicFormat LText.Text where-  magicFormat = Builder.toLazyText+-- | Generic formatter, can format an expression to any @t@ as long as+--   @t@ is an instance of 'IsString'.+fmt :: QuasiQuoter+fmt = templateF pythonDelimiters "fmt" -instance MagicFormat Builder.Builder where-  magicFormat = id+fmtWithDelimiters :: (Char, Char) -> QuasiQuoter+fmtWithDelimiters delimiters = templateF delimiters "fmtWithDelimiters"  pythonDelimiters :: (Char, Char) pythonDelimiters = ('{', '}')
+ src/PyF/Class.hs view
@@ -0,0 +1,58 @@+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE TypeSynonymInstances #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE DefaultSignatures #-}+module PyF.Class where++import Data.Int+import Data.Word+import Numeric.Natural++import qualified Data.Text.Lazy as LText+import qualified Data.Text as SText++-- | The three categories of formatting in 'PyF'+data PyFCategory+  = PyFIntegral+  -- ^ Format as an integral, no fractional part, precise value+  | PyFFractional+  -- ^ Format as a fractional, approximate value with a fractional part+  | PyFString+  -- ^ Format as a string++-- | Classify a type to a 'PyFCategory'+--   This classification will be used to decide which formatting to+--   use when no type specifier in provided.+type family PyFClassify t :: PyFCategory++type instance PyFClassify Integer = 'PyFIntegral+type instance PyFClassify Int = 'PyFIntegral+type instance PyFClassify Int8 = 'PyFIntegral+type instance PyFClassify Int16 = 'PyFIntegral+type instance PyFClassify Int32 = 'PyFIntegral+type instance PyFClassify Int64 = 'PyFIntegral+type instance PyFClassify Natural = 'PyFIntegral+type instance PyFClassify Word = 'PyFIntegral+type instance PyFClassify Word8 = 'PyFIntegral+type instance PyFClassify Word16 = 'PyFIntegral+type instance PyFClassify Word32 = 'PyFIntegral+type instance PyFClassify Word64 = 'PyFIntegral++type instance PyFClassify Float = 'PyFFractional+type instance PyFClassify Double = 'PyFFractional++type instance PyFClassify String = 'PyFString+type instance PyFClassify LText.Text = 'PyFString+type instance PyFClassify SText.Text = 'PyFString++-- | Convert a type to string+--   The default implementation uses `Show`+class PyFToString t where+  pyfToString :: t -> String+  default pyfToString :: Show t => t -> String+  pyfToString = show++instance PyFToString String where pyfToString = id+instance PyFToString LText.Text where pyfToString = LText.unpack+instance PyFToString SText.Text where pyfToString = SText.unpack
src/PyF/Internal/PythonSyntax.hs view
@@ -18,6 +18,7 @@   , TypeFormat(..)   , AlternateForm(..)   , pattern DefaultFormatMode+  , Parser   ) where @@ -85,24 +86,37 @@ parsePythonFormatString exts = parseGenericFormatString exts ('{', '}')  parseGenericFormatString :: [ParseExtension.Extension] -> (Char, Char) -> Parser [Item]-parseGenericFormatString exts delimiters = many ((Raw "\\" <$ "\\\\") <|> (Raw "" <$ "\\\n") <|> rawString delimiters <|> escapedParenthesis delimiters <|> replacementField exts delimiters) <* eof+parseGenericFormatString exts delimiters = many (rawString delimiters <|> escapedParenthesis delimiters <|> replacementField exts delimiters) <* eof  rawString :: (Char, Char) -> Parser Item-rawString (openingChar,closingChar) = Raw . escapeChars <$> some (noneOf ([openingChar, closingChar]))+rawString (openingChar,closingChar) = do+  chars <- some (noneOf ([openingChar, closingChar]))+  case escapeChars chars of+    Left remaining -> do+      offset <- getOffset+      setOffset (offset - length remaining)+      fancyFailure (Set.singleton (ErrorFail "lexical error in literal section"))+    Right escaped -> return (Raw escaped)  escapedParenthesis :: (Char, Char) -> Parser Item escapedParenthesis (openingChar, closingChar) = Raw <$> (parseRaw openingChar <|> parseRaw closingChar)   where parseRaw c = c:[] <$ string (replicate 2 c) -{- | Replace escape chars with their value+{- | Replace escape chars with their value. Results in a Left with the+remainder of the string on encountering a lexical error (such as a bad escape+sequence). >>> escapeChars "hello \\n"-"hello \n"+Right "hello \n"+>>> escapeChars "hello \\x"+Left "\\x" -}-escapeChars :: String -> String-escapeChars "" = ""+escapeChars :: String -> Either String String+escapeChars "" = Right ""+escapeChars ('\\':'\n':xs) = escapeChars xs+escapeChars ('\\':'\\':xs) = ('\\' :) <$> escapeChars xs escapeChars s = case Data.Char.readLitChar s of-                  [] -> ""-                  ((c, xs):_) -> c : escapeChars xs+                  ((c, xs):_) -> (c :) <$> escapeChars xs+                  _ -> Left s  replacementField :: [ParseExtension.Extension] -> (Char, Char) -> Parser Item replacementField exts (charOpening, charClosing) = do@@ -156,11 +170,11 @@   | CharacterF -- ^ Character, will convert an integer to its character representation   | DecimalF SignMode -- ^ Decimal, base 10 integer formatting   | ExponentialF Precision AlternateForm SignMode -- ^ Exponential notation for floatting points-  | ExponentialCapsF Precision AlternateForm SignMode -- ^ Exponential notation with capitalised 'e'+  | ExponentialCapsF Precision AlternateForm SignMode -- ^ Exponential notation with capitalised @e@   | FixedF Precision AlternateForm SignMode -- ^ Fixed number of digits floating point   | FixedCapsF Precision AlternateForm SignMode -- ^ Capitalized version of the previous   | GeneralF Precision AlternateForm SignMode -- ^ General formatting: `FixedF` or `ExponentialF` depending on the number magnitude-  | GeneralCapsF Precision AlternateForm SignMode -- ^ Same as `GeneralF` but with upper case 'E' and infinite / NaN+  | GeneralCapsF Precision AlternateForm SignMode -- ^ Same as `GeneralF` but with upper case @E@ and infinite / NaN   | OctalF AlternateForm SignMode -- ^ Octal, such as 00245   | StringF Precision -- ^ Simple string   | HexF AlternateForm SignMode -- ^ Hexadecimal, such as 0xaf3e@@ -226,32 +240,39 @@    case t of     Nothing -> pure (FormatMode padding (DefaultF prec (fromMaybe Minus s)) grouping)-    Just flag -> case evalFlag flag prec alternateForm s of+    Just flag -> case evalFlag flag padding grouping prec alternateForm s of       Right fmt -> pure (FormatMode padding fmt grouping)       Left typeError -> do         lastCharFailed typeError -evalFlag :: TypeFlag -> Precision -> AlternateForm -> Maybe SignMode -> Either String TypeFormat-evalFlag Flagb prec alt s = failIfPrec prec (BinaryF alt (defSign s))-evalFlag Flagc prec alt s = failIfS s =<< failIfPrec prec =<< failIfAlt alt CharacterF-evalFlag Flagd prec alt s = failIfPrec prec =<< failIfAlt alt (DecimalF (defSign s))-evalFlag Flage prec alt s = pure $ExponentialF prec alt (defSign s)-evalFlag FlagE prec alt s = pure $ ExponentialCapsF prec alt (defSign s)-evalFlag Flagf prec alt s = pure $ FixedF prec alt (defSign s)-evalFlag FlagF prec alt s = pure $ FixedCapsF prec alt (defSign s)-evalFlag Flagg prec alt s = pure $ GeneralF prec alt (defSign s)-evalFlag FlagG prec alt s = pure $ GeneralCapsF prec alt (defSign s)-evalFlag Flagn _prec _alt _s = Left ("Type 'n' not handled (yet). " ++ errgGn)-evalFlag Flago prec alt s = failIfPrec prec $ OctalF alt (defSign s)-evalFlag Flags prec alt s = failIfS s =<< (failIfAlt alt $ StringF prec)-evalFlag Flagx prec alt s = failIfPrec prec $ HexF alt (defSign s)-evalFlag FlagX prec alt s = failIfPrec prec $ HexCapsF alt (defSign s)-evalFlag FlagPercent prec alt s = pure $ PercentF prec alt (defSign s)+evalFlag :: TypeFlag -> Padding -> Maybe Char -> Precision -> AlternateForm -> Maybe SignMode -> Either String TypeFormat+evalFlag Flagb _pad _grouping prec alt s = failIfPrec prec (BinaryF alt (defSign s))+evalFlag Flagc _pad _grouping prec alt s = failIfS s =<< failIfPrec prec =<< failIfAlt alt CharacterF+evalFlag Flagd _pad _grouping prec alt s = failIfPrec prec =<< failIfAlt alt (DecimalF (defSign s))+evalFlag Flage _pad _grouping prec alt s = pure $ExponentialF prec alt (defSign s)+evalFlag FlagE _pad _grouping prec alt s = pure $ ExponentialCapsF prec alt (defSign s)+evalFlag Flagf _pad _grouping prec alt s = pure $ FixedF prec alt (defSign s)+evalFlag FlagF _pad _grouping prec alt s = pure $ FixedCapsF prec alt (defSign s)+evalFlag Flagg _pad _grouping prec alt s = pure $ GeneralF prec alt (defSign s)+evalFlag FlagG _pad _grouping prec alt s = pure $ GeneralCapsF prec alt (defSign s)+evalFlag Flagn _pad _grouping _prec _alt _s = Left ("Type 'n' not handled (yet). " ++ errgGn)+evalFlag Flago _pad _grouping prec alt s = failIfPrec prec $ OctalF alt (defSign s)+evalFlag Flags pad grouping prec alt s = failIfGrouping grouping =<< failIfInsidePadding pad =<< failIfS s =<< (failIfAlt alt $ StringF prec)+evalFlag Flagx _pad _grouping prec alt s = failIfPrec prec $ HexF alt (defSign s)+evalFlag FlagX _pad _grouping prec alt s = failIfPrec prec $ HexCapsF alt (defSign s)+evalFlag FlagPercent _pad _grouping prec alt s = pure $ PercentF prec alt (defSign s)  defSign :: Maybe SignMode -> SignMode defSign Nothing = Minus defSign (Just s) = s +failIfGrouping :: Maybe Char -> TypeFormat -> Either String TypeFormat+failIfGrouping (Just _) _t = Left "String type is incompatible with grouping (_ or ,)."+failIfGrouping Nothing t = Right t++failIfInsidePadding :: Padding -> TypeFormat -> Either String TypeFormat+failIfInsidePadding (Padding _ (Just (_, AnyAlign AlignInside))) _t = Left "String type is incompatible with inside padding (=)."+failIfInsidePadding _ t = Right t  errgGn :: String errgGn = "Use one of {'b', 'c', 'd', 'e', 'E', 'f', 'F', 'g', 'G', 'n', 'o', 's', 'x', 'X', '%'}."
src/PyF/Internal/QQ.hs view
@@ -1,3 +1,4 @@+{-# LANGUAGE TypeFamilies #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE OverloadedStrings #-}@@ -9,10 +10,11 @@ {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE UndecidableInstances #-}+{-# LANGUAGE TypeApplications #-}  {- | This module uses the python mini language detailed in 'PyF.Internal.PythonSyntax' to build an template haskell expression-representing a formatted string ('String', 'Text' or 'Builder').+representing a formatted string.  -} module PyF.Internal.QQ (@@ -26,14 +28,6 @@  import Data.Maybe (fromMaybe) -import qualified Data.Text.Lazy.Builder as Builder--import qualified Data.Text.Lazy as LText-import qualified Data.Text as SText--import qualified Data.Word as Word-import qualified Data.Int as Int-import Numeric.Natural import qualified Data.Maybe  import PyF.Internal.PythonSyntax@@ -43,6 +37,8 @@ import PyF.Formatters (AnyAlign(..)) import Data.Proxy import GHC.TypeLits+import PyF.Class+import Data.String (fromString)  -- Be Careful: empty format string -- | Parse a string and return a formatter for it@@ -50,13 +46,19 @@ toExp delimiters s = do   filename <- loc_filename <$> location -  exts <- Data.Maybe.mapMaybe thExtToMetaExt <$> extsEnabled+  thExts <- extsEnabled+  let exts = Data.Maybe.mapMaybe thExtToMetaExt thExts +  let+    wrapFromString e = if OverloadedStrings `elem` thExts+                       then [| fromString $(e) |]+                       else e+   case parse (parseGenericFormatString exts delimiters) filename s of     Left err -> do       err' <- overrideErrorForFile filename err       fail (errorBundlePretty err')-    Right items -> goFormat items+    Right items -> wrapFromString (goFormat items)  -- Megaparsec displays error relative to what they parsed -- However the formatting string is part of a more complex file and we@@ -74,8 +76,8 @@   let     -- drop the first lines of the file up to the line containing the quasiquote     -- then, split in what is before the QQ and what is after.-    -- e.g.  blablabla [f|hello|] will split to-    -- "blablabla [f|" and "hello|]"+    -- e.g.  blablabla [fmt|hello|] will split to+    -- "blablabla [fmt|" and "hello|]"     (prefix, postfix) = splitAt (col - 1) $ unlines $ drop (line - 1) (lines fileContent)  @@ -90,7 +92,14 @@ toExpPython :: String -> Q Exp toExpPython = toExp ('{', '}') +{-+Note: Empty String Lifting++Empty string are lifter as [] instead of "", so I'm using LitE (String L) instead+-}+ goFormat :: [Item] -> Q Exp+goFormat [] = pure $ LitE (StringL "") -- see [Empty String Lifting] goFormat items = foldl1 fofo <$> (mapM toFormat items)  fofo :: Exp -> Exp -> Exp@@ -99,92 +108,78 @@ -- Real formatting is here  toFormat :: Item -> Q Exp-toFormat (Raw x) = [| Builder.fromString x |]+toFormat (Raw x) = pure $ LitE (StringL x) -- see [Empty String Lifting] toFormat (Replacement expr y) = do   formatExpr <- padAndFormat (fromMaybe DefaultFormatMode y)-  pure (VarE 'Builder.fromString `AppE` (formatExpr `AppE` expr))+  pure (formatExpr `AppE` expr) -changePrec :: Precision -> Maybe Int-changePrec PrecisionDefault = Just 6-changePrec (Precision n) = Just (fromIntegral n)+changePrec :: Precision -> Q Exp+changePrec PrecisionDefault = [| Just 6 |]+changePrec (Precision n) = [| Just n |] -changePrec' :: Precision -> Maybe Int-changePrec' PrecisionDefault = Nothing-changePrec' (Precision n) = Just (fromIntegral n)+changePrec' :: Precision ->  Q Exp+changePrec' PrecisionDefault = [| Nothing |]+changePrec' (Precision n) = [| Just n |] -toGrp :: Maybe b -> a -> Maybe (a, b)-toGrp mb a = (a,) <$> mb+toGrp :: Maybe Char -> Int -> Q Exp+toGrp mb a = [| grp |]+  where grp = (a,) <$> mb  withAlt :: AlternateForm -> Formatters.Format t t' t'' -> Q Exp withAlt NormalForm e = [| e |] withAlt AlternateForm e = [| Formatters.Alternate e |] --- Todo: Alternates for floating padAndFormat :: FormatMode -> Q Exp padAndFormat (FormatMode padding tf grouping) = case tf of   -- Integrals-  BinaryF alt s -> [| formatAnyIntegral $(withAlt alt Formatters.Binary) s (newPadding padding) (toGrp grouping 4) |]-  CharacterF -> [| formatAnyIntegral Formatters.Character Formatters.Minus (newPadding padding) Nothing |]-  DecimalF s -> [| formatAnyIntegral Formatters.Decimal s (newPadding padding) (toGrp grouping 3) |]-  HexF alt s -> [| formatAnyIntegral $(withAlt alt Formatters.Hexa) s (newPadding padding) (toGrp grouping 4) |]-  OctalF alt s -> [| formatAnyIntegral $(withAlt alt Formatters.Octal) s (newPadding padding) (toGrp grouping 4) |]-  HexCapsF alt s -> [| formatAnyIntegral (Formatters.Upper $(withAlt alt Formatters.Hexa)) s (newPadding padding) (toGrp grouping 4) |]+  BinaryF alt s -> [| formatAnyIntegral $(withAlt alt Formatters.Binary) s $(newPaddingQ padding) $(toGrp grouping 4) |]+  CharacterF -> [| formatAnyIntegral Formatters.Character Formatters.Minus $(newPaddingQ padding) Nothing |]+  DecimalF s -> [| formatAnyIntegral Formatters.Decimal s $(newPaddingQ padding) $(toGrp grouping 3) |]+  HexF alt s -> [| formatAnyIntegral $(withAlt alt Formatters.Hexa) s $(newPaddingQ padding) $(toGrp grouping 4) |]+  OctalF alt s -> [| formatAnyIntegral $(withAlt alt Formatters.Octal) s $(newPaddingQ padding) $(toGrp grouping 4) |]+  HexCapsF alt s -> [| formatAnyIntegral (Formatters.Upper $(withAlt alt Formatters.Hexa)) s $(newPaddingQ padding) $(toGrp grouping 4) |]    -- Floating-  ExponentialF prec alt s -> [| formatAnyFractional $(withAlt alt Formatters.Exponent) s (newPadding padding) (toGrp grouping 3) (changePrec prec) |]-  ExponentialCapsF prec alt s -> [| formatAnyFractional (Formatters.Upper $(withAlt alt Formatters.Exponent)) s (newPadding padding) (toGrp grouping 3) (changePrec prec) |]-  GeneralF prec alt s -> [| formatAnyFractional $(withAlt alt Formatters.Generic) s (newPadding padding) (toGrp grouping 3) (changePrec prec) |]-  GeneralCapsF prec alt s -> [| formatAnyFractional (Formatters.Upper $(withAlt alt Formatters.Generic)) s (newPadding padding) (toGrp grouping 3) (changePrec prec) |]-  FixedF prec alt s -> [| formatAnyFractional $(withAlt alt Formatters.Fixed) s (newPadding padding) (toGrp grouping 3) (changePrec prec) |]-  FixedCapsF prec alt s -> [| formatAnyFractional (Formatters.Upper $(withAlt alt Formatters.Fixed)) s (newPadding padding) (toGrp grouping 3) (changePrec prec) |]-  PercentF prec alt s -> [| formatAnyFractional $(withAlt alt Formatters.Percent) s (newPadding padding) (toGrp grouping 3) (changePrec prec) |]+  ExponentialF prec alt s -> [| formatAnyFractional $(withAlt alt Formatters.Exponent) s $(newPaddingQ padding) $(toGrp grouping 3) $(changePrec prec) |]+  ExponentialCapsF prec alt s -> [| formatAnyFractional (Formatters.Upper $(withAlt alt Formatters.Exponent)) s $(newPaddingQ padding) $(toGrp grouping 3) $(changePrec prec) |]+  GeneralF prec alt s -> [| formatAnyFractional $(withAlt alt Formatters.Generic) s $(newPaddingQ padding) $(toGrp grouping 3) $(changePrec prec) |]+  GeneralCapsF prec alt s -> [| formatAnyFractional (Formatters.Upper $(withAlt alt Formatters.Generic)) s $(newPaddingQ padding) $(toGrp grouping 3) $(changePrec prec) |]+  FixedF prec alt s -> [| formatAnyFractional $(withAlt alt Formatters.Fixed) s $(newPaddingQ padding) $(toGrp grouping 3) $(changePrec prec) |]+  FixedCapsF prec alt s -> [| formatAnyFractional (Formatters.Upper $(withAlt alt Formatters.Fixed)) s $(newPaddingQ padding) $(toGrp grouping 3) $(changePrec prec) |]+  PercentF prec alt s -> [| formatAnyFractional $(withAlt alt Formatters.Percent) s $(newPaddingQ padding) $(toGrp grouping 3) $(changePrec prec) |]    -- Default / String--  -- Note: v / i / f uses pat and var to ensure stable name in error message-  DefaultF prec s -> [| \($(pat "v")) ->-      case categorise (Proxy :: Proxy $(typeAllowed)) $(var "v") of-        Integral $(pat "i") -> formatAnyIntegral Formatters.Decimal s (newPadding padding) (toGrp grouping 3) $(var "i")-        Fractional $(pat "f") -> formatAnyFractional Formatters.Generic s (newPadding padding) (toGrp grouping 3) (changePrec' prec) $(var "f")-        StringType $(pat "f") -> Formatters.formatString (newPaddingForString padding) (changePrec' prec) $(var "f")-                         |]-   where-     typeAllowed :: Q Type-     typeAllowed = case padding of-       PaddingDefault -> [t| EnableForString |]-       Padding _ Nothing -> [t| EnableForString |]-       Padding _ (Just (_, AnyAlign a)) -> case Formatters.getAlignForString a of-         Nothing -> [t| DisableForString |]-         Just _ -> [t| EnableForString |]--  StringF prec -> [| Formatters.formatString pad (changePrec' prec) |]-    where pad = newPaddingForString padding---- Generate stable name in TH slices--- The name are postfixed by _PyF to limit the risk of shadowing--pat :: String -> Q Pat-pat name = pure (VarP (mkName (name ++ "_PyF")))--var :: String -> Q Exp-var name = pure (VarE (mkName (name ++ "_PyF")))+  DefaultF prec s -> [| formatAny s $(paddingToPaddingK padding) $(toGrp grouping 3) $(changePrec' prec) |]+  StringF prec -> [| Formatters.formatString (newPaddingKForString $(paddingToPaddingK padding)) $(changePrec' prec) . pyfToString |] -newPaddingForString :: Padding -> Maybe (Int, Formatters.AlignMode 'Formatters.AlignAll, Char)-newPaddingForString padding = case padding of-    PaddingDefault -> Nothing-    Padding i Nothing -> Just (fromIntegral i, Formatters.AlignLeft, ' ') -- default align left and fill with space for string-    Padding i (Just (mc, AnyAlign a)) -> case Formatters.getAlignForString a of-      Nothing -> error alignErrorMsg-      Just al -> pure (fromIntegral i, al, fromMaybe ' ' mc)+newPaddingQ :: Padding -> Q Exp+newPaddingQ pad = [| pad' |]+  where pad' = newPaddingUnQ pad -newPadding :: Padding -> Maybe (Integer, AnyAlign, Char)-newPadding padding = case padding of+newPaddingUnQ :: Padding -> Maybe (Integer, AnyAlign, Char)+newPaddingUnQ padding = case padding of     PaddingDefault -> Nothing     (Padding i al) -> case al of       Nothing -> Just (i, AnyAlign Formatters.AlignRight, ' ') -- Right align and space is default for any object, except string       Just (Nothing, a) -> Just (i, a, ' ')       Just (Just c, a) -> Just (i, a, c) +data PaddingK k where+  PaddingDefaultK :: PaddingK 'Formatters.AlignAll+  PaddingK :: Integer -> (Maybe (Maybe Char, Formatters.AlignMode k)) -> PaddingK k++paddingToPaddingK :: Padding -> Q Exp+paddingToPaddingK p = case p of+  PaddingDefault -> [| PaddingDefaultK |]+  Padding i Nothing -> [| PaddingK i Nothing :: PaddingK 'Formatters.AlignAll |]+  Padding i (Just (c, AnyAlign a)) -> [| PaddingK i (Just (c, a)) |]++paddingKToPadding :: PaddingK k -> Padding+paddingKToPadding p = case p of+  PaddingDefaultK -> PaddingDefault+  PaddingK i Nothing -> Padding i Nothing+  PaddingK i (Just (c, a)) -> Padding i (Just (c, AnyAlign a))+ formatAnyIntegral :: (Show i, Integral i) => Formatters.Format t t' 'Formatters.Integral -> Formatters.SignMode -> Maybe (Integer, AnyAlign, Char) -> Maybe (Int, Char) -> i -> String formatAnyIntegral f s Nothing grouping i = Formatters.formatIntegral f s Nothing grouping i formatAnyIntegral f s (Just (padSize, AnyAlign alignMode, c)) grouping i = Formatters.formatIntegral f s (Just (fromIntegral padSize, alignMode, c)) grouping i@@ -193,44 +188,35 @@ formatAnyFractional f s Nothing grouping p i = Formatters.formatFractional f s Nothing grouping p i formatAnyFractional f s (Just (padSize, AnyAlign alignMode, c)) grouping p i = Formatters.formatFractional f s (Just (fromIntegral padSize, alignMode, c)) grouping p i -data FormattingType where-  StringType :: String -> FormattingType-  Fractional :: RealFloat t => t -> FormattingType-  Integral :: (Show t, Integral t) => t -> FormattingType+class FormatAny i k where+  formatAny :: Formatters.SignMode -> PaddingK k -> Maybe (Int, Char) -> Maybe Int -> i -> String -class Categorise k t where-  categorise :: Proxy k -> t -> FormattingType+instance (FormatAny2 (PyFClassify t) t k) => FormatAny t k where+  formatAny = formatAny2 (Proxy :: Proxy (PyFClassify t)) -instance Categorise k Integer where categorise _  i = Integral i-instance Categorise k Int where categorise _  i = Integral i-instance Categorise k Int.Int8 where categorise _  i = Integral i-instance Categorise k Int.Int16 where categorise _  i = Integral i-instance Categorise k Int.Int32 where categorise _  i = Integral i-instance Categorise k Int.Int64 where categorise _  i = Integral i+class FormatAny2 (c :: PyFCategory) (i :: *) (k :: Formatters.AlignForString) where+  formatAny2 :: Proxy c -> Formatters.SignMode -> PaddingK k -> Maybe (Int, Char) -> Maybe Int -> i -> String -instance Categorise k Natural where categorise _  i = Integral i-instance Categorise k Word where categorise _  i = Integral i-instance Categorise k Word.Word8 where categorise _  i = Integral i-instance Categorise k Word.Word16 where categorise _  i = Integral i-instance Categorise k Word.Word32 where categorise _  i = Integral i-instance Categorise k Word.Word64 where categorise _  i = Integral i+instance (Show t, Integral t) => FormatAny2 'PyFIntegral t k where+  formatAny2 _ s a p _precision i = formatAnyIntegral Formatters.Decimal s (newPaddingUnQ (paddingKToPadding a)) p i -instance Categorise k Float where categorise _  f = Fractional f-instance Categorise k Double where categorise _  f = Fractional f+instance (RealFloat t) => FormatAny2 'PyFFractional t k where+  formatAny2 _ s a p precision t = formatAnyFractional Formatters.Generic s (newPaddingUnQ (paddingKToPadding a)) p precision t --- This may use DataKinds extension, however the need for the--- extension will leak inside the code calling the template haskell--- quasi quotes.-data EnableForString-data DisableForString+newPaddingKForString :: PaddingK 'Formatters.AlignAll -> Maybe (Int, Formatters.AlignMode 'Formatters.AlignAll, Char)+newPaddingKForString padding = case padding of+    PaddingDefaultK -> Nothing+    PaddingK i Nothing -> Just (fromIntegral i, Formatters.AlignLeft, ' ') -- default align left and fill with space for string+    PaddingK i (Just (mc, a)) -> Just (fromIntegral i, a, fromMaybe ' ' mc) -instance Categorise EnableForString LText.Text where categorise _  t = StringType (LText.unpack t)-instance Categorise EnableForString SText.Text where categorise _  t = StringType (SText.unpack t)-instance Categorise EnableForString String where categorise _  t = StringType t -alignErrorMsg :: String-alignErrorMsg = "String Cannot be aligned with the inside `=` mode"+-- TODO: _s(ign) and _grouping should trigger errors+instance (PyFToString t) => FormatAny2 'PyFString t 'Formatters.AlignAll where+  formatAny2 _ _s a _grouping precision t = Formatters.formatString (newPaddingKForString a) precision (pyfToString t) -instance TypeError ('Text "String Cannot be aligned with the inside `=` mode") => Categorise DisableForString LText.Text where categorise _ _ = error "unreachable"-instance TypeError ('Text "String Cannot be aligned with the inside `=` mode") => Categorise DisableForString SText.Text where categorise _ _ = error "unreachable"-instance TypeError ('Text "String Cannot be aligned with the inside `=` mode") => Categorise DisableForString String where categorise _ _ = error "unreachable"+instance TypeError ('Text "String type is incompatible with inside padding (=).") => FormatAny2 'PyFString t 'Formatters.AlignNumber where+  formatAny2 = error "Unreachable"++type family ToFmt t where+  ToFmt 'PyFIntegral = 'Formatters.Integral+  ToFmt 'PyFFractional = 'Formatters.Fractional
test/Spec.hs view
@@ -1,19 +1,21 @@-{-# OPTIONS -Wno-type-defaults #-}-{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE ExtendedDefaultRules #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeApplications #-} {-# LANGUAGE BinaryLiterals #-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE DeriveAnyClass, GeneralizedNewtypeDeriving, DerivingStrategies #-}  import Test.Hspec  import PyF import SpecUtils import SpecCustomDelimiters+import Data.Text  {--   - Normal tests are done using the recommanded API: [f|.....|]+   - Normal tests are done using the recommanded API: [fmt|.....|]    - Test with $(checkExample formatString result) are checked against the python reference implementation. Result is provided as documentation.    - Test with $(checkExampleDiff formatString result) are not checked against the python reference implementation. This is known (and documented) differences.    - Test with $(check formatString) are only tested against the python reference implementation.@@ -22,6 +24,27 @@ main :: IO () main = hspec spec +newtype FooFloating = FooFloating Float+  deriving newtype (Show, RealFloat, RealFrac, Floating, Fractional, Real, Enum, Num, Ord, Eq)++newtype FooIntegral = FooIntegral Integer+  deriving newtype (Show, Integral, Real, Enum, Num, Ord, Eq)++data Foo = Foo++data FooDefault = FooDefault+  deriving (Show)++instance PyFToString FooDefault++instance PyFToString Foo where+  pyfToString Foo = "I'm a Foo"++type instance PyFClassify Foo = 'PyFString+type instance PyFClassify FooFloating = 'PyFFractional+type instance PyFClassify FooIntegral = 'PyFIntegral+type instance PyFClassify FooDefault = 'PyFString+ spec :: Spec spec = do   describe "simple with external variable" $ do@@ -29,9 +52,9 @@       anInt = 123       aFloat = 0.234       aString = "hello"-    it "int" $ [f|{anInt}|] `shouldBe` "123"-    it "float" $ [f|{aFloat}|] `shouldBe` "0.234"-    it "string" $ [f|{aString}|] `shouldBe` "hello"+    it "int" $ [fmt|{anInt}|] `shouldBe` "123"+    it "float" $ [fmt|{aFloat}|] `shouldBe` "0.234"+    it "string" $ [fmt|{aString}|] `shouldBe` "hello"   describe "only expression" $ do     describe "default" $ do       it "int" $(checkExample "{123}" "123")@@ -178,7 +201,7 @@         age = 31         euroToFrancs = 6.55957       in-        [f|hello {name} you are {age} years old and the conversion rate of euro is {euroToFrancs:.2}|] `shouldBe` ("hello Guillaume you are 31 years old and the conversion rate of euro is 6.56")+        [fmt|hello {name} you are {age} years old and the conversion rate of euro is {euroToFrancs:.2}|] `shouldBe` ("hello Guillaume you are 31 years old and the conversion rate of euro is 6.56")     describe "error reporting" $ do@@ -186,14 +209,14 @@    describe "sub expressions" $ do     it "works" $ do-      [f|2pi = {2 * pi:.2}|] `shouldBe` "2pi = 6.28"+      [fmt|2pi = {2 * pi:.2}|] `shouldBe` "2pi = 6.28"    describe "escape strings" $ do     it "works" $ do-      [f|hello \n\b|] `shouldBe` "hello \n\b"+      [fmt|hello \n\b|] `shouldBe` "hello \n\b"    it "escape chars" $ do-     [f|}}{{}}{{|] `shouldBe` "}{}{"+     [fmt|}}{{}}{{|] `shouldBe` "}{}{"    describe "custom delimiters" $ do     it "works" $ do@@ -201,24 +224,60 @@     it "escape chars" $ do        [myCustomFormatter|@@!!@@!!|] `shouldBe` "@!@!" +  describe "empty line" $ do+    it "works" $ do+      [fmt||] `shouldBe` ""    describe "multi line escape" $ do     it "works" $ do-      [f|\+      [fmt|\ - a - b \ |] `shouldBe` "- a\n- b\n" +    it "escapes in middle of line" $ do+      [fmt|Example goes \+here!|] `shouldBe` "Example goes here!"++    it "escapes a lot of things" $ do+      [fmt|\+I'm a line with \n and \\ and a correct line+ending, but that one is escaped\+And I'm escaping before and after: \\{pi:.3f}\\+yeah\+|] `shouldBe` "I'm a line with \n and \\ and a correct line\nending, but that one is escapedAnd I'm escaping before and after: \\3.142\\\nyeah"+     it "escapes" $ do-      [f|\\+      [fmt|\\ - a - b \ |] `shouldBe` "\\\n- a\n- b\n" +  describe "empty trailing value" $ do+    it "String" $ do+      ([fmt|\+{pi:.0}+|] :: String) `shouldBe` "3\n"+   describe "language extensions" $ do      it "parses @Int" $ do-       [f|hello {show @Int 10}|] `shouldBe` "hello 10"+       [fmt|hello {show @Int 10}|] `shouldBe` "hello 10"      it "parses BinaryLiterals" $ do-       [f|hello {0b1111}|] `shouldBe` "hello 15"+       [fmt|hello {0b1111}|] `shouldBe` "hello 15"+++  describe "custom types" $ do+      it "works with integral" $ do+        [fmt|{FooIntegral 10:d}|] `shouldBe` "10"+      it "works with floating" $ do+        [fmt|{FooFloating 25.123:f}|] `shouldBe` "25.123000"+      it "works with string" $ do+        [fmt|{Foo:s}|] `shouldBe` "I'm a Foo"+        [fmt|{FooDefault:s}|] `shouldBe` "FooDefault"+      it "works with classify" $ do+        [fmt|{Foo}|] `shouldBe` "I'm a Foo"+        [fmt|{FooIntegral 100}|] `shouldBe` "100"+        [fmt|{FooFloating 100.123}|] `shouldBe` "100.123"+        [fmt|{FooDefault}|] `shouldBe` "FooDefault"
test/SpecCustomDelimiters.hs view
@@ -5,4 +5,4 @@ import PyF  myCustomFormatter :: QuasiQuoter-myCustomFormatter = fWithDelimiters ('@','!')+myCustomFormatter = fmtWithDelimiters ('@','!')
test/SpecFail.hs view
@@ -29,14 +29,17 @@   | Ok String   deriving (Show, Eq) +makeTemplate :: String -> String+makeTemplate s = "{-# LANGUAGE QuasiQuotes, ExtendedDefaultRules, TypeApplications #-}\nimport PyF\ntruncate' = truncate @Float @Int\nhello = \"hello\"\nnumber = 3.14 :: Float\nmain :: IO ()\nmain = putStrLn [fmt|" ++ s ++ "|]\n"+ {- | Compile a formatting string ->>> checkCompile "pi:x"+>>> checkCompile fileContent CompileError "Bla bla bla, Floating cannot be formatted as hexa (`x`) -}-checkCompile :: String -> IO CompilationStatus-checkCompile s = withSystemTempFile "PyFTest.hs" $ \path fd -> do-  IO.hPutStr fd $ "{-# LANGUAGE QuasiQuotes, ExtendedDefaultRules, TypeApplications #-}\nimport PyF\ntruncate' = truncate @Float @Int\nhello = \"hello\"\nnumber = 3.14 :: Float\nmain :: IO ()\nmain = [f|" ++ s ++ "|]\n"+checkCompile :: HasCallStack => String -> IO CompilationStatus+checkCompile content = withSystemTempFile "PyFTest.hs" $ \path fd -> do+  IO.hPutStr fd content   IO.hFlush fd    (ecode, _stdout, stderr) <- readProcessWithExitCode "ghc" [path,@@ -104,12 +107,22 @@  -- if the compilation fails, runs a golden test on compilation output -- else, fails the test+fileFailCompile :: HasCallStack => FilePath -> Spec+fileFailCompile path = do+  fileContent <- runIO $ readFile path++  -- I'm using the hash of the path, considering that the file content can evolve+  failCompileContent (hash path) path fileContent+ failCompile :: HasCallStack => String -> Spec-failCompile s = do-  before (checkCompile s) $ it (s ++ " " ++ show (hash s)) $ \res -> case res of-   CompileError output -> golden (show $ hash s) output-   _ -> assertFailure (show res)+failCompile s = failCompileContent (hash s) s (makeTemplate s) +failCompileContent :: HasCallStack => Int -> String -> String -> Spec+failCompileContent h caption fileContent = do+  before (checkCompile fileContent) $ it (show caption) $ \res -> case res of+   CompileError output -> golden (show h) output+   _ -> assertFailure (show $ ".golden/" <> show h  <> "\n" <>show res)+ main :: IO () main = hspec spec @@ -134,8 +147,7 @@         failCompile "{hello:=100s}"         failCompile "{hello:=100}" -      -- XXX: this are not failing for now, it should be fixed-      xdescribe "grouping" $ do+      describe "grouping" $ do         failCompile "{hello:_s}"         failCompile "{hello:,s}" @@ -171,6 +183,11 @@     xdescribe "not specified" $ do       failCompile "{truncate number:.3}"       failCompile "{hello:#}"+      failCompile "{hello:+}"+      failCompile "{hello: }"+      failCompile "{hello:-}"+      failCompile "{hello:_}"+      failCompile "{hello:,}"      describe "multiples lines" $ do       failCompile "hello\n\n\n{pi:l}"@@ -187,3 +204,17 @@      describe "fail is not enabled extension" $ do       failCompile "{0b0001}"++    describe "lexical errors" $ do+      failCompile "foo\\Pbar"++    describe "fileFailures" $ do+      mapM_ fileFailCompile [+        "test/failureCases/bug18.hs"+        ]++    describe "Wrong type" $ do+      failCompile "{True:s}"+      failCompile "{True}"+      failCompile "{True:f}"+      failCompile "{True:d}"
+ test/SpecOverloaded.hs view
@@ -0,0 +1,22 @@+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE OverloadedStrings #-}++import Test.Hspec++import PyF+import Data.Text+import Data.ByteString++main :: IO ()+main = hspec spec++spec :: Spec+spec = do+  describe "Test formatting with different types" $ do+    it "String" $ do+      [fmt|hello {10:d}|] `shouldBe` ("hello 10" :: String)+    it "Text" $ do+      [fmt|hello {10:d}|] `shouldBe` ("hello 10" :: Text)+    it "ByteString" $ do+      [fmt|hello {10:d}|] `shouldBe` ("hello 10" :: ByteString)