PyF 0.6.0.2 → 0.6.1.0
raw patch · 9 files changed
+112/−28 lines, 9 filesPVP: major bump suggested
API removals or changes: PVP suggests a major version bump
API changes (from Hackage documentation)
+ PyF: f'WithDelimiters :: (Char, Char) -> QuasiQuoter
+ PyF: fBuilderWithDelimiters :: (Char, Char) -> QuasiQuoter
+ PyF: fIOWithDelimiters :: (Char, Char) -> QuasiQuoter
+ PyF: fLazyTextWithDelimiters :: (Char, Char) -> QuasiQuoter
+ PyF: fStrictTextWithDelimiters :: (Char, Char) -> QuasiQuoter
+ PyF: fStringWithDelimiters :: (Char, Char) -> QuasiQuoter
+ PyF: fWithDelimiters :: (Char, Char) -> QuasiQuoter
+ PyF.Internal.PythonSyntax: parseGenericFormatString :: (Char, Char) -> Parser [Item]
+ PyF.Internal.QQ: toExpPython :: String -> Q Exp
- PyF.Internal.QQ: toExp :: String -> Q Exp
+ PyF.Internal.QQ: toExp :: (Char, Char) -> String -> Q Exp
Files
- ChangeLog.md +4/−0
- PyF.cabal +2/−2
- Readme.md +31/−0
- src/PyF.hs +34/−10
- src/PyF/Internal/PythonSyntax.hs +15/−10
- src/PyF/Internal/QQ.hs +8/−4
- test/Spec.hs +7/−0
- test/SpecCustomDelimiters.hs +8/−0
- test/SpecUtils.hs +3/−2
ChangeLog.md view
@@ -1,5 +1,9 @@ # Revision history for FormatStringLiteral +## 0.6.1.0 -- 2018-08-03++- Custom delimiters, you can use whatever delimiters you want in place of `{` and `}`.+ ## 0.6.0.0 -- 2018-08-02 - Fix the espace parsing of `{{` and `}}` as `{` and `}`
PyF.cabal view
@@ -1,5 +1,5 @@ name: PyF-version: 0.6.0.2+version: 0.6.1.0 synopsis: Quasiquotations for a python like interpolated string formater description: Quasiquotations for a python like interpolated string formater. license: BSD3@@ -39,7 +39,7 @@ type: exitcode-stdio-1.0 hs-source-dirs: test main-is: Spec.hs- other-modules: SpecUtils+ other-modules: SpecUtils SpecCustomDelimiters build-depends: base, PyF, hspec, text, template-haskell, formatting, process ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N default-language: Haskell2010
Readme.md view
@@ -241,6 +241,35 @@ ... ``` +## Custom Delimiters++If `{` and `}` does not fit your needs, for example if you are formatting a lot of json, you can use custom delimiters. All quasi quoters have a parametric form which accepts custom delimiters. Due to template haskell stage restriction, you must define your custom quasi quoter in an other module.++For example, in `MyCustomDelimiter.hs`:++```haskell+module MyCustomQQ where++import Language.Haskell.TH.Quote++import PyF++myCustomFormatter :: QuasiQuoter+myCustomFormatter = fStringWithDelimiters ('@','!')+```++Later, in another module:++```haskell+import MyCustomQQ++-- ...++[myCustomFormatter|pi = @pi:2.f!|]+```++Escaping still works by doubling the delimiters, `@@!!@@!!` will be formatted as `@!@!`.+ ## Difference with the Python Syntax The implementation is unit-tested against the reference python implementation (python 3.6.4) and should match its result. However some formatters are not supported or some (minor) differences can be observed.@@ -256,6 +285,7 @@ - General formatters *g* and *G* behaves a bit differently. Precision influence the number of significant digits instead of the number of the magnitude at which the representation changes between fixed and exponential. - Grouping options allows grouping with an `_` for floating point, python only allows `,`.+- Custom delimiters # Build / test @@ -274,6 +304,7 @@ - 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
src/PyF.hs view
@@ -12,6 +12,15 @@ fLazyText, fStrictText, + -- * With custom delimiters+ fWithDelimiters,+ f'WithDelimiters,+ fIOWithDelimiters,+ fStringWithDelimiters,+ fBuilderWithDelimiters,+ fLazyTextWithDelimiters,+ fStrictTextWithDelimiters,+ -- * Formatting re-export runFormat, format,@@ -32,9 +41,9 @@ import qualified Data.Text as SText import qualified Data.Text.Lazy.Builder as Builder -templateF :: String -> QuasiQuoter-templateF fName = QuasiQuoter {- quoteExp = QQ.toExp+templateF :: (Char, Char) -> String -> QuasiQuoter+templateF delimiters fName = QuasiQuoter {+ quoteExp = QQ.toExp delimiters , quotePat = err "pattern" , quoteType = err "type" , quoteDec = err "declaration"@@ -44,12 +53,18 @@ -- | Returns an expression usable with Formatting.format (and similar functions) f :: QuasiQuoter-f = templateF "f"+f = templateF pythonDelimiters "f" +fWithDelimiters :: (Char, Char) -> QuasiQuoter+fWithDelimiters delimiters = templateF delimiters "fWithDelimiters"+ -- | Generic formatter, can format an expression to (lazy) Text, String, Builder and IO () depending on type inference f' :: QuasiQuoter-f' = wrapQQ (templateF "f'") (VarE 'magicFormat)+f' = wrapQQ (templateF pythonDelimiters "f'") (VarE 'magicFormat) +f'WithDelimiters :: (Char, Char) -> QuasiQuoter+f'WithDelimiters delimiters = templateF delimiters "f'WithDelimiters"+ wrapQQ :: QuasiQuoter -> Exp -> QuasiQuoter wrapQQ qq wrap = qq { quoteExp = \s -> do@@ -77,19 +92,28 @@ -- Monomorphic formatters fIO, fString, fStrictText, fLazyText, fBuilder :: QuasiQuoter+fIOWithDelimiters, fStringWithDelimiters, fStrictTextWithDelimiters, fLazyTextWithDelimiters, fBuilderWithDelimiters :: (Char, Char) -> QuasiQuoter +fIO = fIOWithDelimiters pythonDelimiters+fString = fStringWithDelimiters pythonDelimiters+fStrictText = fStrictTextWithDelimiters pythonDelimiters+fLazyText = fLazyTextWithDelimiters pythonDelimiters+fBuilder = fBuilderWithDelimiters pythonDelimiters +pythonDelimiters :: (Char, Char)+pythonDelimiters = ('{', '}')+ -- | Format the format string and directly print it to stdout-fIO = wrapQQ (templateF "fIO") (VarE 'F.fprint)+fIOWithDelimiters delimiters = wrapQQ (templateF delimiters "fIO") (VarE 'F.fprint) -- | Format the format string as a 'String'-fString = wrapQQ (templateF "fString") (VarE 'F.formatToString)+fStringWithDelimiters delimiters = wrapQQ (templateF delimiters "fString") (VarE 'F.formatToString) -- | Format the format string as a strict 'SText.Text'-fStrictText = wrapQQ (templateF "fStrictTeext") (VarE 'F.sformat)+fStrictTextWithDelimiters delimiters = wrapQQ (templateF delimiters "fStrictTeext") (VarE 'F.sformat) -- | Format the format string as a Lazy 'LText.Text'-fLazyText = wrapQQ (templateF "fLazy") (VarE 'F.sformat)+fLazyTextWithDelimiters delimiters = wrapQQ (templateF delimiters "fLazy") (VarE 'F.sformat) -- | Format the format string as a 'Builder.Builder'-fBuilder = wrapQQ (templateF "fBuilder") (VarE 'F.bprint)+fBuilderWithDelimiters delimiters = wrapQQ (templateF delimiters "fBuilder") (VarE 'F.bprint)
src/PyF/Internal/PythonSyntax.hs view
@@ -10,6 +10,7 @@ -} module PyF.Internal.PythonSyntax ( parsePythonFormatString+ , parseGenericFormatString , Item(..) , FormatMode(..) , Padding(..)@@ -77,14 +78,18 @@ Nothing))] -} parsePythonFormatString :: Parser [Item]-parsePythonFormatString = many (rawString <|> escapedParenthesis <|> replacementField)+parsePythonFormatString = parseGenericFormatString ('{', '}') -rawString :: Parser Item-rawString = Raw . escapeChars <$> some (noneOf ("{}" :: [Char]))+parseGenericFormatString :: (Char, Char) -> Parser [Item]+parseGenericFormatString delimiters = many (rawString delimiters <|> escapedParenthesis delimiters <|> replacementField delimiters) -escapedParenthesis :: Parser Item-escapedParenthesis = Raw <$> (("{" <$ string "{{") <|> ("}" <$ string "}}"))+rawString :: (Char, Char) -> Parser Item+rawString (openingChar,closingChar) = Raw . escapeChars <$> some (noneOf ([openingChar, closingChar])) +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 >>> escapeChars "hello \\n" "hello \n"@@ -95,14 +100,14 @@ [] -> "" ((c, xs):_) -> c : escapeChars xs -replacementField :: Parser Item-replacementField = do- _ <- char '{'- expr <- many (noneOf ("}:" :: [Char]))+replacementField :: (Char, Char) -> Parser Item+replacementField (charOpening, charClosing) = do+ _ <- char charOpening+ expr <- many (noneOf (charClosing:":")) fmt <- optional $ do _ <- char ':' format_spec- _ <- char '}'+ _ <- char charClosing pure (Replacement expr fmt)
src/PyF/Internal/QQ.hs view
@@ -14,7 +14,8 @@ -} module PyF.Internal.QQ (- toExp)+ toExp,+ toExpPython) where import Text.Megaparsec@@ -45,8 +46,8 @@ -- Be Careful: empty format string -- | Parse a string and return a formatter for it-toExp:: String -> Q Exp-toExp s = do+toExp:: (Char, Char) -> String -> Q Exp+toExp delimiters s = do filename <- loc_filename <$> location (line, col) <- loc_start <$> location @@ -55,7 +56,7 @@ (SourcePos sName _ _) NonEmpty.:| xs = statePos currentState in currentState {statePos = (SourcePos sName (mkPos line) (mkPos col)) NonEmpty.:| xs} - case parse (updateParserState (change_log filename) >> parsePythonFormatString) filename s of+ case parse (updateParserState (change_log filename) >> parseGenericFormatString delimiters) filename s of Left err -> do if filename == "<interactive>"@@ -65,6 +66,9 @@ fileContent <- runIO (readFile filename) fail (parseErrorPretty' fileContent err) Right items -> goFormat items++toExpPython :: String -> Q Exp+toExpPython = toExp ('{', '}') goFormat :: [Item] -> Q Exp goFormat items = foldl1 fofo <$> (mapM toFormat items)
test/Spec.hs view
@@ -8,6 +8,7 @@ import PyF import SpecUtils+import SpecCustomDelimiters {- - Normal tests are done using the recommanded API: [fString|.....|]@@ -187,3 +188,9 @@ it "escape chars" $ do [fString|}}{{}}{{|] `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` "@!@!"
+ test/SpecCustomDelimiters.hs view
@@ -0,0 +1,8 @@+module SpecCustomDelimiters where++import Language.Haskell.TH.Quote++import PyF++myCustomFormatter :: QuasiQuoter+myCustomFormatter = fStringWithDelimiters ('@','!')
test/SpecUtils.hs view
@@ -43,6 +43,7 @@ This expression is a failure if python cannot format this formatString or if the python result does not match the (provided) reference. -}+ pyCheck :: String -> Maybe String -> Q Exp pyCheck s exampleStr = do pythonRes <- Language.Haskell.TH.Syntax.runIO (runPythonExample s)@@ -50,7 +51,7 @@ case pythonRes of Nothing -> [| expectationFailure $ "Expression: `" ++ s ++ "` fails in python" |] Just res -> do- let qexp = [| formatToString $(toExp s) `shouldBe` res |]+ let qexp = [| formatToString $(toExpPython s) `shouldBe` res |] case exampleStr of Nothing -> qexp Just e -> if res == e@@ -71,7 +72,7 @@ against the python implementation -} checkExampleDiff :: String -> String -> Q Exp-checkExampleDiff s res = [| formatToString $(toExp s) `shouldBe` res |]+checkExampleDiff s res = [| formatToString $(toExpPython s) `shouldBe` res |] {- | `check formatString` checks only with the python implementation -}