packages feed

Frames 0.5.1 → 0.6.0

raw patch · 8 files changed

+95/−40 lines, 8 filesdep ~basePVP ok

version bump matches the API change (PVP)

Dependency ranges changed: base

API changes (from Hackage documentation)

+ Frames.CSV: produceTokens :: MonadSafe m => FilePath -> Separator -> Producer [Text] m ()
- Frames: pipeTable :: (ReadRec rs, RMap rs, Monad m) => Pipe Text (Record rs) m ()
+ Frames: pipeTable :: (ReadRec rs, RMap rs, Monad m) => Pipe [Text] (Record rs) m ()
- Frames: pipeTableMaybe :: (Monad m, ReadRec rs, RMap rs) => Pipe Text (Rec (Maybe :. ElField) rs) m ()
+ Frames: pipeTableMaybe :: (Monad m, ReadRec rs, RMap rs) => Pipe [Text] (Rec (Maybe :. ElField) rs) m ()
- Frames.CSV: pipeTable :: (ReadRec rs, RMap rs, Monad m) => Pipe Text (Record rs) m ()
+ Frames.CSV: pipeTable :: (ReadRec rs, RMap rs, Monad m) => Pipe [Text] (Record rs) m ()
- Frames.CSV: pipeTableMaybe :: (Monad m, ReadRec rs, RMap rs) => Pipe Text (Rec (Maybe :. ElField) rs) m ()
+ Frames.CSV: pipeTableMaybe :: (Monad m, ReadRec rs, RMap rs) => Pipe [Text] (Rec (Maybe :. ElField) rs) m ()
- Frames.CSV: pipeTableMaybeOpt :: (Monad m, ReadRec rs, RMap rs) => ParserOptions -> Pipe Text (Rec (Maybe :. ElField) rs) m ()
+ Frames.CSV: pipeTableMaybeOpt :: (Monad m, ReadRec rs, RMap rs) => ParserOptions -> Pipe [Text] (Rec (Maybe :. ElField) rs) m ()
- Frames.CSV: pipeTableOpt :: (ReadRec rs, RMap rs, Monad m) => ParserOptions -> Pipe Text (Record rs) m ()
+ Frames.CSV: pipeTableOpt :: (ReadRec rs, RMap rs, Monad m) => ParserOptions -> Pipe [Text] (Record rs) m ()
- Frames.CSV: prefixInference :: (ColumnTypeable a, Monoid a, Monad m) => ParserOptions -> Parser Text m [a]
+ Frames.CSV: prefixInference :: (ColumnTypeable a, Monoid a, Monad m) => Parser [Text] m [a]
- Frames.CSV: readColHeaders :: (ColumnTypeable a, Monoid a, Monad m) => ParserOptions -> Producer Text m () -> m [(Text, a)]
+ Frames.CSV: readColHeaders :: (ColumnTypeable a, Monoid a, Monad m) => ParserOptions -> Producer [Text] m () -> m [(Text, a)]
- Frames.CSV: readFileLatin1Ln :: MonadSafe m => FilePath -> Producer Text m ()
+ Frames.CSV: readFileLatin1Ln :: MonadSafe m => FilePath -> Producer [Text] m ()
- Frames.TH: RowGen :: [String] -> String -> Separator -> String -> Proxy a -> Producer Text (SafeT IO) () -> RowGen
+ Frames.TH: RowGen :: [String] -> String -> Separator -> String -> Proxy a -> Separator -> Producer [Text] (SafeT IO) () -> RowGen
- Frames.TH: [lineReader] :: RowGen -> Producer Text (SafeT IO) ()
+ Frames.TH: [lineReader] :: RowGen -> Separator -> Producer [Text] (SafeT IO) ()
- Frames.TH: colNamesP :: Monad m => ParserOptions -> Producer Text m () -> m [Text]
+ Frames.TH: colNamesP :: Monad m => Producer [Text] m () -> m [Text]

Files

CHANGELOG.md view
@@ -1,3 +1,13 @@+# 0.6.0+Support external CSV tokenizers++Internal functionality is now defined more cleanly atop a stream of rows already broken into columns (rather than a stream of rows that we quietly break into columns ourself). This permits the use of external parsers such as provided by the new [Frames-dsv](https://hackage.haskell.org/package/Frames-dsv) package that supplies a CSV parser built atop `hw-dsv`.++The built-in CSV parser remains for ease of installation.++# 0.5.1+GHC 8.6 compatibility+ # 0.5.0  - Renamed the `rgetf` and `rputf` exported by the `Frames` module to `rgetField` and `rputField`. This avoids clashing with the same names exported by `vinyl` and further advances the process of eliminating the old `Frames` `Col` type in favor of `vinyl`'s `ElField`.
Frames.cabal view
@@ -1,5 +1,5 @@ name:                Frames-version:             0.5.1+version:             0.6.0 synopsis:            Data frames For working with tabular data files description:         User-friendly, type safe, runtime efficient tooling for                      working with tabular data deserialized from@@ -11,7 +11,7 @@ license-file:        LICENSE author:              Anthony Cowley maintainer:          acowley@gmail.com-copyright:           Copyright (C) 2014-2015 Anthony Cowley+copyright:           Copyright (C) 2014-2018 Anthony Cowley category:            Data build-type:          Simple extra-source-files:  benchmarks/*.hs benchmarks/*.py@@ -25,6 +25,7 @@                      test/data/prestigeNoHeader.csv                      test/data/prestigePartial.csv                      test/data/catSmall.csv test/data/catLarge.csv+                     test/data/multiline.csv                      data/left1.csv data/right1.csv data/left_summary.csv                      data/FL2.csv cabal-version:       >=1.10
data/GetData.hs view
@@ -2,6 +2,7 @@ import Codec.Archive.Zip import qualified Data.ByteString.Lazy.Char8 as B import Data.Maybe (fromJust)+import Data.Monoid (First(..)) import Network.HTTP.Client import System.Directory (createDirectoryIfMissing) @@ -28,6 +29,13 @@                     httpLbs req m >>=                         B.writeFile "data/adult.csv"                       . B.append colNames+                      . B.unlines+                      . map (\ln -> maybe ln id+                                    . getFirst+                                    $ foldMap (First . ($ ln))+                                        [ B.stripSuffix ", <=50K"+                                        , B.stripSuffix ", >50K" ])+                      . B.lines                       . responseBody   where Just req = parseUrlThrow "http://archive.ics.uci.edu/ml/machine-learning-databases/adult/adult.data"         colNames = "age, workclass, fnlwgt, education, education-num, \
src/Frames/CSV.hs view
@@ -8,7 +8,7 @@ -- to those types. module Frames.CSV where import Control.Exception (try, IOException)-import Control.Monad (when)+import Control.Monad (when, unless) import qualified Data.ByteString.Char8 as B8 import qualified Data.Foldable as F import Data.List (intercalate)@@ -140,25 +140,35 @@ -- | Infer column types from a prefix (up to 1000 lines) of a CSV -- file. prefixInference :: (ColumnTypeable a, Monoid a, Monad m)-                => ParserOptions-                -> P.Parser T.Text m [a]-prefixInference opts = P.draw >>= \case+                => P.Parser [T.Text] m [a]+prefixInference = P.draw >>= \case   Nothing -> return []   Just row1 -> P.foldAll (\ts -> zipWith (<>) ts . inferCols)                          (inferCols row1)                          id-  where inferCols = map inferType . tokenizeRow opts+  where inferCols = map inferType  -- | Extract column names and inferred types from a CSV file. readColHeaders :: (ColumnTypeable a, Monoid a, Monad m)-               => ParserOptions -> P.Producer T.Text m () -> m [(T.Text, a)]+               => ParserOptions -> P.Producer [T.Text] m () -> m [(T.Text, a)] readColHeaders opts = P.evalStateT $-  do headerRow <- maybe ((tokenizeRow opts-                         . fromMaybe (error "Empty Producer has no header row")) <$> P.draw)+  do headerRow <- maybe (fromMaybe err <$> P.draw)                         pure                         (headerOverride opts)-     colTypes <- prefixInference opts+     colTypes <- prefixInference+     unless (length headerRow == length colTypes) (error errNumColumns)      return (zip headerRow colTypes)+  where err = error "Empty Producer has no header row"+        errNumColumns =+          unlines+          [ ""+          , "Error parsing CSV: "+          , "  Number of columns in header differs from number of columns"+          , "  found in the remaining file. This may be due to newlines"+          , "  being present within the data itself (not just separating"+          , "  rows). If support for embedded newlines is required, "+          , "  consider using the Frames-dsv package in conjunction with"+          , "  Frames to make use of a different CSV parser."]  -- * Loading CSV Data @@ -194,6 +204,16 @@ produceTextLines :: P.MonadSafe m => FilePath -> P.Producer T.Text m () produceTextLines = pipeLines (try . T.hGetLine) +-- | Produce lines of tokens that were separated by the given+-- separator.+produceTokens :: P.MonadSafe m+              => FilePath+              -> Separator+              -> P.Producer [T.Text] m ()+produceTokens fp sep = produceTextLines fp >-> P.map tokenize+  where tokenize = tokenizeRow popts+        popts = defaultParser { columnSeparator = sep }+ -- | Consume lines of 'T.Text', writing them to a file. consumeTextLines :: P.MonadSafe m => FilePath -> P.Consumer T.Text m r consumeTextLines fp = Safe.withFile fp WriteMode $ \h ->@@ -202,8 +222,9 @@  -- | Produce the lines of a latin1 (or ISO8859 Part 1) encoded file as -- ’T.Text’ values.-readFileLatin1Ln :: P.MonadSafe m => FilePath -> P.Producer T.Text m ()-readFileLatin1Ln = pipeLines (try . fmap T.decodeLatin1 . B8.hGetLine)+readFileLatin1Ln :: P.MonadSafe m => FilePath -> P.Producer [T.Text] m ()+readFileLatin1Ln fp = pipeLines (try . fmap T.decodeLatin1 . B8.hGetLine) fp+                      >-> P.map (tokenizeRow defaultParser)  -- | Read a 'RecF' from one line of CSV. readRow :: ReadRec rs@@ -216,17 +237,19 @@                   -> FilePath                   -> P.Producer (Rec (Maybe :. ElField) rs) m () readTableMaybeOpt opts csvFile =-  produceTextLines csvFile >-> pipeTableMaybeOpt opts+  produceTokens csvFile (columnSeparator opts) >-> pipeTableMaybeOpt opts {-# INLINE readTableMaybeOpt #-}  -- | Stream lines of CSV data into rows of ’Rec’ values values where -- any given entry can fail to parse. pipeTableMaybeOpt :: (Monad m, ReadRec rs, RMap rs)                   => ParserOptions-                  -> P.Pipe T.Text (Rec (Maybe :. ElField) rs) m ()+                  -> P.Pipe [T.Text] (Rec (Maybe :. ElField) rs) m () pipeTableMaybeOpt opts = do   when (isNothing (headerOverride opts)) (() <$ P.await)-  P.map (rmap (either (const (Compose Nothing)) (Compose . Just) . getCompose) . readRow opts)+  P.map (rmap (either (const (Compose Nothing))+                      (Compose . Just) . getCompose)+         . readRec) {-# INLINE pipeTableMaybeOpt #-}  -- | Stream lines of CSV data into rows of ’Rec’ values values where@@ -249,7 +272,7 @@ -- | Stream lines of CSV data into rows of ’Rec’ values where any -- given entry can fail to parse. pipeTableMaybe :: (Monad m, ReadRec rs, RMap rs)-               => P.Pipe T.Text (Rec (Maybe :. ElField) rs) m ()+               => P.Pipe [T.Text] (Rec (Maybe :. ElField) rs) m () pipeTableMaybe = pipeTableMaybeOpt defaultParser {-# INLINE pipeTableMaybe #-} @@ -296,7 +319,7 @@ -- | Pipe lines of CSV text into rows for which each column was -- successfully parsed. pipeTableOpt :: (ReadRec rs, RMap rs, Monad m)-             => ParserOptions -> P.Pipe T.Text (Record rs) m ()+             => ParserOptions -> P.Pipe [T.Text] (Record rs) m () pipeTableOpt opts = pipeTableMaybeOpt opts >-> P.map recMaybe >-> P.concat {-# INLINE pipeTableOpt #-} @@ -310,7 +333,7 @@ -- | Pipe lines of CSV text into rows for which each column was -- successfully parsed. pipeTable :: (ReadRec rs, RMap rs, Monad m)-          => P.Pipe T.Text (Record rs) m ()+          => P.Pipe [T.Text] (Record rs) m () pipeTable = pipeTableOpt defaultParser {-# INLINE pipeTable #-} 
src/Frames/InCore.hs view
@@ -1,13 +1,6 @@-{-# LANGUAGE PolyKinds #-}-{-# LANGUAGE BangPatterns,-             CPP,-             DataKinds,-             EmptyCase,-             FlexibleInstances,-             ScopedTypeVariables,-             TupleSections,-             TypeFamilies,-             TypeOperators,+{-# LANGUAGE BangPatterns, CPP, DataKinds, EmptyCase,+             FlexibleInstances, PolyKinds, ScopedTypeVariables,+             TupleSections, TypeFamilies, TypeOperators,              UndecidableInstances #-} -- | Efficient in-memory (in-core) storage of tabular data. module Frames.InCore where
src/Frames/TH.hs view
@@ -150,8 +150,9 @@            -- ^ A record field that mentions the phantom type list of            -- possible column types. Having this field prevents record            -- update syntax from losing track of the type argument.-         , lineReader :: P.Producer T.Text (P.SafeT IO) ()-           -- ^ A producer of lines of ’T.Text’xs+         , lineReader :: Separator -> P.Producer [T.Text] (P.SafeT IO) ()+           -- ^ A producer of rows of ’T.Text’ values that were+           -- separated by a 'Separator' value.          }  -- -- | Shorthand for a 'Proxy' value of 'ColumnUniverse' applied to the@@ -164,12 +165,12 @@ -- separator (a comma), infer column types from the default 'Columns' -- set of types, and produce a row type with name @Row@. rowGen :: FilePath -> RowGen CommonColumns-rowGen = RowGen [] "" defaultSep "Row" Proxy . produceTextLines+rowGen = RowGen [] "" defaultSep "Row" Proxy . produceTokens  -- | Like 'rowGen', but will also generate custom data types for -- 'Categorical' variables with up to 8 distinct variants. rowGenCat :: FilePath -> RowGen CommonColumnsCat-rowGenCat = RowGen [] "" defaultSep "Row" Proxy . produceTextLines+rowGenCat = RowGen [] "" defaultSep "Row" Proxy . produceTokens  -- -- | Generate a type for each row of a table. This will be something -- -- like @Record ["x" :-> a, "y" :-> b, "z" :-> c]@.@@ -202,12 +203,11 @@ --         colNames' | null columnNames = Nothing --                   | otherwise = Just (map T.pack columnNames) --         opts = ParserOptions colNames' separator (RFC4180Quoting '\"')---         lineSource = lineReader >-> P.take prefixSize+--         lineSource = lineReader separator >-> P.take prefixSize  -- | Tokenize the first line of a ’P.Producer’.-colNamesP :: Monad m-          => ParserOptions -> P.Producer T.Text m () -> m [T.Text]-colNamesP opts src = either (const []) (tokenizeRow opts . fst) <$> P.next src+colNamesP :: Monad m => P.Producer [T.Text] m () -> m [T.Text]+colNamesP src = either (const []) fst <$> P.next src  -- | Generate a type for a row of a table all of whose columns remain -- unparsed 'Text' values.@@ -216,7 +216,7 @@                 => RowGen a -> DecsQ tableTypesText' (RowGen {..}) =   do colNames <- runIO . P.runSafeT $-                 maybe (colNamesP opts lineReader)+                 maybe (colNamesP (lineReader separator))                        pure                        (headerOverride opts)      let headers = zip colNames (repeat (ConT ''T.Text))@@ -266,7 +266,7 @@   where colNames' | null columnNames = Nothing                   | otherwise = Just (map T.pack columnNames)         opts = ParserOptions colNames' separator (RFC4180Quoting '\"')-        lineSource = lineReader P.>-> P.take prefixSize+        lineSource = lineReader separator P.>-> P.take prefixSize         mkColDecs :: T.Text -> Either (String -> Q [Dec]) Type -> Q (Type, [Dec])         mkColDecs colNm colTy = do           let safeName = tablePrefix ++ (T.unpack . sanitizeTypeName $ colNm)
test/Spec.hs view
@@ -1,6 +1,8 @@ {-# LANGUAGE CPP, DataKinds, OverloadedStrings, QuasiQuotes,-             TemplateHaskell, TypeOperators #-}+             ScopedTypeVariables, TemplateHaskell, TypeApplications,+             TypeOperators #-} module Main (manualGeneration, main) where+import Control.Exception (ErrorCall, catch) import Control.Monad (unless) import Data.Functor.Identity import Data.Char@@ -12,6 +14,7 @@ import Language.Haskell.TH.Syntax (addDependentFile) import Frames import Frames.CSV (produceCSV)+import Frames.CSV (defaultParser, produceTokens, defaultSep, readColHeaders) import DataCSV import Pipes.Prelude (toListM) import PrettyTH@@ -189,3 +192,12 @@          mCustom <- H.runIO Categorical.fifthMonthCustom          it "Can parse into manually-specified categorical variables" $            mCustom `shouldBe` Just Categorical.MyMay+       describe "Detects parse failures" $ do+         caught <- H.runIO $+           (runSafeT $ do+             _ <- readColHeaders @Columns+                    defaultParser+                    (produceTokens "test/data/multiline.csv" defaultSep)+             return False)+            `catch` \(_ :: ErrorCall) -> return True+         it "Fails on embedded newlines" caught
+ test/data/multiline.csv view
@@ -0,0 +1,8 @@+RowNum,Description,X,Y+1,"simple",10,10+2,"""quoted""",20,20+3,"multi+line+text+field",30,30+4,"simple again",40,40