PyF 0.8.0.2 → 0.8.1.0
raw patch · 7 files changed
+109/−42 lines, 7 filesdep +mtlPVP: major bump suggested
API removals or changes: PVP suggests a major version bump
Dependencies added: mtl
API changes (from Hackage documentation)
- PyF.Internal.PythonSyntax: instance Language.Haskell.TH.Syntax.Lift PyF.Internal.PythonSyntax.Padding
- PyF.Internal.PythonSyntax: instance Language.Haskell.TH.Syntax.Lift PyF.Internal.PythonSyntax.Precision
- PyF.Internal.PythonSyntax: parsePythonFormatString :: [Extension] -> Parser [Item]
+ PyF.Internal.PythonSyntax: HaskellExpr :: Exp -> ExprOrValue t
+ PyF.Internal.PythonSyntax: ParsingContext :: (Char, Char) -> [Extension] -> ParsingContext
+ PyF.Internal.PythonSyntax: Value :: t -> ExprOrValue t
+ PyF.Internal.PythonSyntax: [delimiters] :: ParsingContext -> (Char, Char)
+ PyF.Internal.PythonSyntax: [enabledExtensions] :: ParsingContext -> [Extension]
+ PyF.Internal.PythonSyntax: data ExprOrValue t
+ PyF.Internal.PythonSyntax: data ParsingContext
+ PyF.Internal.PythonSyntax: instance GHC.Show.Show PyF.Internal.PythonSyntax.ParsingContext
+ PyF.Internal.PythonSyntax: instance GHC.Show.Show t => GHC.Show.Show (PyF.Internal.PythonSyntax.ExprOrValue t)
- PyF.Internal.PythonSyntax: Precision :: Integer -> Precision
+ PyF.Internal.PythonSyntax: Precision :: ExprOrValue Integer -> Precision
- PyF.Internal.PythonSyntax: parseGenericFormatString :: [Extension] -> (Char, Char) -> Parser [Item]
+ PyF.Internal.PythonSyntax: parseGenericFormatString :: Parser [Item]
- PyF.Internal.PythonSyntax: type Parser t = Parsec Void String t
+ PyF.Internal.PythonSyntax: type Parser t = ParsecT Void String (Reader ParsingContext) t
Files
- ChangeLog.md +4/−0
- PyF.cabal +2/−1
- Readme.md +10/−1
- src/PyF/Internal/PythonSyntax.hs +51/−21
- src/PyF/Internal/QQ.hs +22/−17
- test/Spec.hs +8/−0
- test/SpecFail.hs +12/−2
ChangeLog.md view
@@ -1,5 +1,9 @@ # Revision history for PyF +## 0.8.1.0 -- 2019-09-03++- Precision can now be any arbitrary haskell expression, such as `[fmt|hello pi = {pi:.{1 + 3}}|]`.+ ## 0.8.0.2 -- 2019-08-27 - (minor bugfix in tests): Use python3 instead of "python" to help build on environment with both python2 and python3
PyF.cabal view
@@ -1,6 +1,6 @@ cabal-version: 2.4 name: PyF-version: 0.8.0.2+version: 0.8.1.0 synopsis: Quasiquotations for a python like interpolated string formater description: Quasiquotations for a python like interpolated string formater. license: BSD-3-Clause@@ -27,6 +27,7 @@ , megaparsec >= 7.0 && < 8.0 , text >= 0.11 && < 1.3 , containers >= 0.5 && < 0.7+ , mtl -- haskell-src-meta < 0.8.2 does not correctly handle TypeApplication -- That's not critical for PyF, so you can ignore this bound if needed
Readme.md view
@@ -139,6 +139,15 @@ Will returns `-a\n-b`. Note how the first and last line breaks are ignored. +## Arbitrary value for precision++The precision field can be any haskell expression instead of a fixed number:++```haskell+>>> [fmt|{pi:.{1+2}}|]+3.142+```+ # Output type *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.@@ -268,7 +277,7 @@ ### Not supported - 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 support sub variables in the formatting options in every places, such as `{expression:.{precision}}`. We only support it for `precision`. This is more complexe to setup for others fields. - 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.
src/PyF/Internal/PythonSyntax.hs view
@@ -9,8 +9,7 @@ This module provides a parser for <https://docs.python.org/3.4/library/string.html#formatspec python format string mini language>. -} module PyF.Internal.PythonSyntax- ( parsePythonFormatString- , parseGenericFormatString+ ( parseGenericFormatString , Item(..) , FormatMode(..) , Padding(..)@@ -19,6 +18,8 @@ , AlternateForm(..) , pattern DefaultFormatMode , Parser+ , ParsingContext(..)+ , ExprOrValue(..) ) where @@ -28,6 +29,7 @@ import qualified Text.Megaparsec.Char.Lexer as L import Text.Megaparsec.Char import Data.Void (Void)+import Control.Monad.Reader import qualified Data.Char @@ -40,8 +42,14 @@ import qualified Language.Haskell.Exts.SrcLoc as SrcLoc import PyF.Formatters -type Parser t = Parsec Void String t+type Parser t = ParsecT Void String (Reader ParsingContext) t +data ParsingContext = ParsingContext+ { delimiters :: (Char, Char)+ , enabledExtensions :: [ParseExtension.Extension]+ }+ deriving (Show)+ {- -- TODO: - Better parsing of integer@@ -82,14 +90,14 @@ (FixedF (Precision 2) NormalForm Minus) Nothing))] -}-parsePythonFormatString :: [ParseExtension.Extension] -> Parser [Item]-parsePythonFormatString exts = parseGenericFormatString exts ('{', '}') -parseGenericFormatString :: [ParseExtension.Extension] -> (Char, Char) -> Parser [Item]-parseGenericFormatString exts delimiters = many (rawString delimiters <|> escapedParenthesis delimiters <|> replacementField exts delimiters) <* eof+parseGenericFormatString :: Parser [Item]+parseGenericFormatString = do+ many (rawString <|> escapedParenthesis <|> replacementField) <* eof -rawString :: (Char, Char) -> Parser Item-rawString (openingChar,closingChar) = do+rawString :: Parser Item+rawString = do+ (openingChar, closingChar) <- delimiters <$> ask chars <- some (noneOf ([openingChar, closingChar])) case escapeChars chars of Left remaining -> do@@ -98,8 +106,11 @@ 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)+escapedParenthesis :: Parser Item+escapedParenthesis = do+ (openingChar, closingChar) <- delimiters <$> ask++ Raw <$> (parseRaw openingChar <|> parseRaw closingChar) where parseRaw c = c:[] <$ string (replicate 2 c) {- | Replace escape chars with their value. Results in a Left with the@@ -118,8 +129,11 @@ ((c, xs):_) -> (c :) <$> escapeChars xs _ -> Left s -replacementField :: [ParseExtension.Extension] -> (Char, Char) -> Parser Item-replacementField exts (charOpening, charClosing) = do+replacementField :: Parser Item+replacementField = do+ exts <- enabledExtensions <$> ask+ (charOpening, charClosing) <- delimiters <$> ask+ _ <- char charOpening expr <- evalExpr exts (many (noneOf (charClosing:":" :: [Char]))) fmt <- optional $ do@@ -142,9 +156,15 @@ | Padding Integer (Maybe (Maybe Char, AnyAlign)) deriving (Show) +-- | Represents a value of type @t@ or an Haskell expression supposed to represents that value+data ExprOrValue t+ = Value t+ | HaskellExpr Exp+ deriving (Show)+ -- | Floating point precision data Precision = PrecisionDefault- | Precision Integer+ | Precision (ExprOrValue Integer) deriving (Show) {- @@ -231,7 +251,7 @@ grouping <- optional grouping_option - prec <- option PrecisionDefault (char '.' *> (Precision <$> precision))+ prec <- option PrecisionDefault parsePrecision t <- optional type_ let padding = case w of@@ -245,6 +265,17 @@ Left typeError -> do lastCharFailed typeError +parsePrecision :: Parser Precision+parsePrecision = do+ exts <- enabledExtensions <$> ask+ (charOpening, charClosing) <- delimiters <$> ask++ _ <- char '.'+ choice [+ Precision . Value <$> precision,+ char charOpening *> (Precision . HaskellExpr <$> evalExpr exts (manyTill anySingle (char charClosing)))+ ]+ 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@@ -279,7 +310,11 @@ failIfPrec :: Precision -> TypeFormat -> Either String TypeFormat failIfPrec PrecisionDefault i = Right i-failIfPrec (Precision i) _ = Left ("Type incompatible with precision (." ++ show i ++ "), use any of {'e', 'E', 'f', 'F', 'g', 'G', 'n', 's', '%'} or remove the precision field.")+failIfPrec (Precision e) _ = Left ("Type incompatible with precision (." ++ showExpr ++ "), use any of {'e', 'E', 'f', 'F', 'g', 'G', 'n', 's', '%'} or remove the precision field.")+ where+ showExpr = case e of+ Value v -> show v+ HaskellExpr expr -> show expr failIfAlt :: AlternateForm -> TypeFormat -> Either String TypeFormat failIfAlt NormalForm i = Right i@@ -353,8 +388,3 @@ FlagX <$ char 'X', FlagPercent <$ char '%' ]--- -- TODO: remove !-deriving instance Lift Precision-deriving instance Lift Padding
src/PyF/Internal/QQ.hs view
@@ -29,6 +29,7 @@ import Data.Maybe (fromMaybe) import qualified Data.Maybe+import Control.Monad.Reader import PyF.Internal.PythonSyntax import PyF.Internal.Extensions@@ -43,7 +44,7 @@ -- Be Careful: empty format string -- | Parse a string and return a formatter for it toExp:: (Char, Char) -> String -> Q Exp-toExp delimiters s = do+toExp expressionDelimiters s = do filename <- loc_filename <$> location thExts <- extsEnabled@@ -54,7 +55,8 @@ then [| fromString $(e) |] else e - case parse (parseGenericFormatString exts delimiters) filename s of+ let context = ParsingContext expressionDelimiters exts+ case runReader (runParserT parseGenericFormatString filename s) context of Left err -> do err' <- overrideErrorForFile filename err fail (errorBundlePretty err')@@ -113,13 +115,16 @@ formatExpr <- padAndFormat (fromMaybe DefaultFormatMode y) pure (formatExpr `AppE` expr) -changePrec :: Precision -> Q Exp-changePrec PrecisionDefault = [| Just 6 |]-changePrec (Precision n) = [| Just n |]+-- | Default precision for floating point+defaultFloatPrecision :: Maybe Int+defaultFloatPrecision = Just 6 -changePrec' :: Precision -> Q Exp-changePrec' PrecisionDefault = [| Nothing |]-changePrec' (Precision n) = [| Just n |]+-- | Precision to maybe+splicePrecision :: Maybe Int -> Precision -> Q Exp+splicePrecision def PrecisionDefault = [| def |]+splicePrecision _ (Precision p) = case p of+ Value n -> [| Just n |]+ HaskellExpr e -> [| Just $(pure e) |] toGrp :: Maybe Char -> Int -> Q Exp toGrp mb a = [| grp |]@@ -140,17 +145,17 @@ 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 $(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) |]+ ExponentialF prec alt s -> [| formatAnyFractional $(withAlt alt Formatters.Exponent) s $(newPaddingQ padding) $(toGrp grouping 3) $(splicePrecision defaultFloatPrecision prec) |]+ ExponentialCapsF prec alt s -> [| formatAnyFractional (Formatters.Upper $(withAlt alt Formatters.Exponent)) s $(newPaddingQ padding) $(toGrp grouping 3) $(splicePrecision defaultFloatPrecision prec) |]+ GeneralF prec alt s -> [| formatAnyFractional $(withAlt alt Formatters.Generic) s $(newPaddingQ padding) $(toGrp grouping 3) $(splicePrecision defaultFloatPrecision prec) |]+ GeneralCapsF prec alt s -> [| formatAnyFractional (Formatters.Upper $(withAlt alt Formatters.Generic)) s $(newPaddingQ padding) $(toGrp grouping 3) $(splicePrecision defaultFloatPrecision prec) |]+ FixedF prec alt s -> [| formatAnyFractional $(withAlt alt Formatters.Fixed) s $(newPaddingQ padding) $(toGrp grouping 3) $(splicePrecision defaultFloatPrecision prec) |]+ FixedCapsF prec alt s -> [| formatAnyFractional (Formatters.Upper $(withAlt alt Formatters.Fixed)) s $(newPaddingQ padding) $(toGrp grouping 3) $(splicePrecision defaultFloatPrecision prec) |]+ PercentF prec alt s -> [| formatAnyFractional $(withAlt alt Formatters.Percent) s $(newPaddingQ padding) $(toGrp grouping 3) $(splicePrecision defaultFloatPrecision prec) |] -- Default / String- DefaultF prec s -> [| formatAny s $(paddingToPaddingK padding) $(toGrp grouping 3) $(changePrec' prec) |]- StringF prec -> [| Formatters.formatString (newPaddingKForString $(paddingToPaddingK padding)) $(changePrec' prec) . pyfToString |]+ DefaultF prec s -> [| formatAny s $(paddingToPaddingK padding) $(toGrp grouping 3) $(splicePrecision Nothing prec) |]+ StringF prec -> [| Formatters.formatString (newPaddingKForString $(paddingToPaddingK padding)) $(splicePrecision Nothing prec) . pyfToString |] newPaddingQ :: Padding -> Q Exp newPaddingQ pad = [| pad' |]
test/Spec.hs view
@@ -215,14 +215,22 @@ it "works" $ do [fmt|hello \n\b|] `shouldBe` "hello \n\b" + describe "variable precision" $ do+ it "works" $ do+ let n = 3 :: Int+ [fmt|{pi:.{n}}|] `shouldBe` "3.142"+ it "escape chars" $ do [fmt|}}{{}}{{|] `shouldBe` "}{}{" + describe "custom delimiters" $ do it "works" $ do [myCustomFormatter|2 * pi = @2*pi:.2f!|] `shouldBe` "2 * pi = 6.28" it "escape chars" $ do [myCustomFormatter|@@!!@@!!|] `shouldBe` "@!@!"+ it "works for custom precision" $ do+ [myCustomFormatter|@pi:.@2!!|] `shouldBe` "3.14" describe "empty line" $ do it "works" $ do
test/SpecFail.hs view
@@ -49,7 +49,17 @@ -- Tests use a filename in a temporary directory which may have a long filename which triggers -- line wrapping, reducing the reproducibility of error message -- By setting the column size to a high value, we ensure reproducible error messages- "-dppr-cols=10000000000000"+ "-dppr-cols=10000000000000",+ -- Clean package environment+ "-hide-all-packages",+ "-package base",+ "-package megaparsec",+ "-package text",+ "-package template-haskell",+ "-package haskell-src-exts",+ "-package haskell-src-meta",+ "-package mtl",+ "-package containers" ] "" case ecode of ExitFailure _ -> pure (CompileError (sanitize path stderr))@@ -67,7 +77,7 @@ -- strip the filename let t = Text.pack s- in Text.unpack (Text.replace (Text.pack path) (Text.pack "INITIALPATH") t) + in Text.unpack (Text.replace (Text.pack path) (Text.pack "INITIALPATH") t) golden :: HasCallStack => String -> String -> IO () golden name output = do