diff --git a/bench/Main.hs b/bench/Main.hs index ee4f508..d95e791 100644 --- a/bench/Main.hs +++ b/bench/Main.hs @@ -1,7 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} module Main where -import Test.Tasty hiding (defaultMain) import Test.Tasty.Bench import Python.Inline @@ -10,7 +9,7 @@ import Python.Inline.QQ main :: IO () main = withPython $ do - py_int <- [pye| 123456 |] + py_int <- runPy [pye| 123456 |] defaultMain - [ bench "FromPy Int" $ whnfIO $ fromPy' @Int py_int + [ bench "FromPy Int" $ whnfIO $ runPy $ fromPy' @Int py_int ] diff --git a/inline-python.cabal b/inline-python.cabal index 8c48018..3f6bba4 100644 --- a/inline-python.cabal +++ b/inline-python.cabal @@ -56,6 +56,7 @@ Library , template-haskell -any , text >=2 , bytestring + , exceptions >=0.10 hs-source-dirs: src include-dirs: include c-sources: cbits/python.c @@ -84,6 +85,7 @@ library test , tasty >=1.2 , tasty-hunit >=0.10 , tasty-quickcheck >=0.10 + , exceptions hs-source-dirs: test Exposed-modules: TST.Run diff --git a/src/Python/Inline.hs b/src/Python/Inline.hs index 15e7be2..cbe6942 100644 --- a/src/Python/Inline.hs +++ b/src/Python/Inline.hs @@ -5,6 +5,8 @@ module Python.Inline , finalizePython , withPython -- * Core data types + , Py + , runPy , PyObject , PyError(..) -- * Conversion between haskell and python diff --git a/src/Python/Inline/Literal.hs b/src/Python/Inline/Literal.hs index cec1292..f314347 100644 --- a/src/Python/Inline/Literal.hs +++ b/src/Python/Inline/Literal.hs @@ -13,6 +13,7 @@ module Python.Inline.Literal ) where import Control.Monad +import Control.Monad.Catch import Control.Monad.IO.Class import Control.Monad.Trans.Class import Control.Monad.Trans.Cont @@ -81,30 +82,30 @@ class FromPy a where -- | Convert python object to haskell value. All python exceptions -- which happen during execution will be converted to @PyError@. -fromPyEither :: FromPy a => PyObject -> IO (Either PyError a) -fromPyEither py = runPy $ unsafeWithPyObject py $ \p -> - (Right <$> basicFromPy p) `catchPy` (pure . Left) +fromPyEither :: FromPy a => PyObject -> Py (Either PyError a) +fromPyEither py = unsafeWithPyObject py $ \p -> + (Right <$> basicFromPy p) `catch` (pure . Left) -- | Convert python object to haskell value. Will return @Nothing@ if -- 'BadPyType' or 'OutOfRange' is thrown. Other python exceptions -- are rethrown. -fromPy :: FromPy a => PyObject -> IO (Maybe a) -fromPy py = runPy $ unsafeWithPyObject py $ \p -> - (Just <$> basicFromPy p) `catchPy` \case +fromPy :: FromPy a => PyObject -> Py (Maybe a) +fromPy py = unsafeWithPyObject py $ \p -> + (Just <$> basicFromPy p) `catch` \case BadPyType -> pure Nothing OutOfRange -> pure Nothing - e -> throwPy e + e -> throwM e -- | Convert python object to haskell value. Throws exception on -- failure. -fromPy' :: FromPy a => PyObject -> IO a -fromPy' py = runPy $ unsafeWithPyObject py basicFromPy +fromPy' :: FromPy a => PyObject -> Py a +fromPy' py = unsafeWithPyObject py basicFromPy -- | Convert haskell value to a python object. -toPy :: ToPy a => a -> IO PyObject -toPy a = runPy $ basicToPy a >>= \case - NULL -> throwPy =<< convertPy2Haskell +toPy :: ToPy a => a -> Py PyObject +toPy a = basicToPy a >>= \case + NULL -> throwM =<< convertPy2Haskell p -> newPyObject p @@ -211,34 +212,34 @@ instance FromPy Int8 where basicFromPy p = basicFromPy @Int64 p >>= \case i | i <= fromIntegral (maxBound :: Int8) , i >= fromIntegral (minBound :: Int8) -> pure $! fromIntegral i - | otherwise -> throwPy OutOfRange + | otherwise -> throwM OutOfRange instance FromPy Int16 where basicFromPy p = basicFromPy @Int64 p >>= \case i | i <= fromIntegral (maxBound :: Int16) , i >= fromIntegral (minBound :: Int16) -> pure $! fromIntegral i - | otherwise -> throwPy OutOfRange + | otherwise -> throwM OutOfRange instance FromPy Int32 where basicFromPy p = basicFromPy @Int64 p >>= \case i | i <= fromIntegral (maxBound :: Int32) , i >= fromIntegral (minBound :: Int32) -> pure $! fromIntegral i - | otherwise -> throwPy OutOfRange + | otherwise -> throwM OutOfRange instance FromPy Word8 where basicFromPy p = basicFromPy @Word64 p >>= \case i | i <= fromIntegral (maxBound :: Word8) -> pure $! fromIntegral i - | otherwise -> throwPy OutOfRange + | otherwise -> throwM OutOfRange instance FromPy Word16 where basicFromPy p = basicFromPy @Word64 p >>= \case i | i <= fromIntegral (maxBound :: Word16) -> pure $! fromIntegral i - | otherwise -> throwPy OutOfRange + | otherwise -> throwM OutOfRange instance FromPy Word32 where basicFromPy p = basicFromPy @Word64 p >>= \case i | i <= fromIntegral (maxBound :: Word32) -> pure $! fromIntegral i - | otherwise -> throwPy OutOfRange + | otherwise -> throwM OutOfRange -- | Encoded as 1-character string @@ -272,7 +273,7 @@ instance FromPy Char where } return -1; } |] - if | r < 0 -> throwPy BadPyType + if | r < 0 -> throwM BadPyType | otherwise -> pure $ chr $ fromIntegral r instance ToPy Bool where @@ -301,7 +302,7 @@ instance (FromPy a, FromPy b) => FromPy (a,b) where inline_py_unpack_iterable($(PyObject *p_tup), 2, $(PyObject **p_args)) }|] lift $ do checkThrowPyError - when (unpack_ok /= 0) $ throwPy BadPyType + when (unpack_ok /= 0) $ throwM BadPyType -- Parse each element of tuple p_a <- takeOwnership =<< liftIO (peekElemOff p_args 0) p_b <- takeOwnership =<< liftIO (peekElemOff p_args 1) @@ -325,7 +326,7 @@ instance (FromPy a, FromPy b, FromPy c) => FromPy (a,b,c) where inline_py_unpack_iterable($(PyObject *p_tup), 3, $(PyObject **p_args)) }|] lift $ do checkThrowPyError - when (unpack_ok /= 0) $ throwPy BadPyType + when (unpack_ok /= 0) $ throwM BadPyType -- Parse each element of tuple p_a <- takeOwnership =<< liftIO (peekElemOff p_args 0) p_b <- takeOwnership =<< liftIO (peekElemOff p_args 1) @@ -352,7 +353,7 @@ instance (FromPy a, FromPy b, FromPy c, FromPy d) => FromPy (a,b,c,d) where inline_py_unpack_iterable($(PyObject *p_tup), 4, $(PyObject **p_args)) }|] lift $ do checkThrowPyError - when (unpack_ok /= 0) $ throwPy BadPyType + when (unpack_ok /= 0) $ throwM BadPyType -- Parse each element of tuple p_a <- takeOwnership =<< liftIO (peekElemOff p_args 0) p_b <- takeOwnership =<< liftIO (peekElemOff p_args 1) @@ -376,14 +377,14 @@ instance (FromPy a) => FromPy [a] where } return iter; } |] - when (nullPtr == p_iter) $ throwPy BadPyType + when (nullPtr == p_iter) $ throwM BadPyType -- let loop f = do p <- Py [C.exp| PyObject* { PyIter_Next($(PyObject* p_iter)) } |] checkThrowPyError case p of NULL -> pure f - _ -> do a <- basicFromPy p `finallyPy` decref p + _ -> do a <- basicFromPy p `finally` decref p loop (f . (a:)) ($ []) <$> loop id @@ -463,7 +464,7 @@ instance (FromPy a1, FromPy a2, ToPy b) => ToPy (a1 -> a2 -> IO b) where -- | Execute haskell callback function pyCallback :: Program (Ptr PyObject) (Ptr PyObject) -> IO (Ptr PyObject) -pyCallback io = unPy $ ensureGIL $ evalContT io `catchPy` convertHaskell2Py +pyCallback io = unPy $ ensureGIL $ evalContT io `catch` convertHaskell2Py -- | Load argument from python object for haskell evaluation loadArg @@ -473,11 +474,11 @@ loadArg -> Int64 -- ^ Total number of arguments -> Program (Ptr PyObject) a loadArg p (fromIntegral -> i) (fromIntegral -> tot) = ContT $ \success -> do - tryPy (basicFromPy p) >>= \case + try (basicFromPy p) >>= \case Right a -> success a Left BadPyType -> oops Left OutOfRange -> oops - Left e -> throwPy e + Left e -> throwM e where oops = Py [CU.block| PyObject* { char err[256]; diff --git a/src/Python/Inline/QQ.hs b/src/Python/Inline/QQ.hs index dc49815..e8bfa8c 100644 --- a/src/Python/Inline/QQ.hs +++ b/src/Python/Inline/QQ.hs @@ -20,10 +20,10 @@ import Python.Internal.Eval -- defined in this block will remain visible. This quasiquote -- doesn't return any python value. -- --- This quote creates object of type @IO ()@ +-- This quote creates object of type @Py ()@ pymain :: QuasiQuoter pymain = QuasiQuoter - { quoteExp = \txt -> [| runPy $ evaluatorPymain $(expQQ Exec txt) |] + { quoteExp = \txt -> [| evaluatorPymain $(expQQ Exec txt) |] , quotePat = error "quotePat" , quoteType = error "quoteType" , quoteDec = error "quoteDec" @@ -33,10 +33,10 @@ pymain = QuasiQuoter -- defined in this block will be discarded. This quasiquote doesn't -- return any python value. -- --- This quote creates object of type @IO ()@ +-- This quote creates object of type @Py ()@ py_ :: QuasiQuoter py_ = QuasiQuoter - { quoteExp = \txt -> [| runPy $ evaluatorPy_ $(expQQ Exec txt) |] + { quoteExp = \txt -> [| evaluatorPy_ $(expQQ Exec txt) |] , quotePat = error "quotePat" , quoteType = error "quoteType" , quoteDec = error "quoteDec" @@ -45,10 +45,10 @@ py_ = QuasiQuoter -- | Evaluate single python expression. It only accepts single -- expressions same as python's @eval@. -- --- This quote creates object of type @IO PyObject@ +-- This quote creates object of type @Py PyObject@ pye :: QuasiQuoter pye = QuasiQuoter - { quoteExp = \txt -> [| runPy $ evaluatorPye $(expQQ Eval txt) |] + { quoteExp = \txt -> [| evaluatorPye $(expQQ Eval txt) |] , quotePat = error "quotePat" , quoteType = error "quoteType" , quoteDec = error "quoteDec" @@ -60,7 +60,7 @@ pye = QuasiQuoter -- call return pyf :: QuasiQuoter pyf = QuasiQuoter - { quoteExp = \txt -> [| runPy $ evaluatorPyf $(expQQ Fun txt) |] + { quoteExp = \txt -> [| evaluatorPyf $(expQQ Fun txt) |] , quotePat = error "quotePat" , quoteType = error "quoteType" , quoteDec = error "quoteDec" diff --git a/src/Python/Internal/Eval.hs b/src/Python/Internal/Eval.hs index 98d6b57..0b3e14b 100644 --- a/src/Python/Internal/Eval.hs +++ b/src/Python/Internal/Eval.hs @@ -30,7 +30,7 @@ module Python.Internal.Eval ) where import Control.Concurrent -import Control.Exception +import Control.Monad.Catch import Control.Monad.IO.Class import Control.Monad.Trans.Cont import Foreign.Ptr @@ -132,18 +132,20 @@ runPy py where -- We check whether interpreter is initialized. Throw exception if -- it wasn't. Better than segfault isn't it? - go = mask_ $ checkInitialized >> unPy (ensureGIL py) + go = mask_ $ isInitialized >>= \case + True -> unPy (ensureGIL py) + False -> error "Python is not initialized" -- | Execute python action. This function is unsafe and should be only -- called in thread of interpreter. unPy :: Py a -> IO a unPy (Py io) = io -checkInitialized :: IO () -checkInitialized = - [CU.exp| int { !Py_IsFinalizing() && Py_IsInitialized() } |] >>= \case - 0 -> error "Python is not initialized" - _ -> pure () + +isInitialized :: IO Bool +isInitialized = do + i <- [CU.exp| int { !Py_IsFinalizing() && Py_IsInitialized() } |] + pure $! i /= 0 @@ -258,7 +260,7 @@ ensureGIL action = do -- PyGILState_STATE is defined as enum. Let hope it will stay -- this way. gil_state <- Py [CU.exp| int { PyGILState_Ensure() } |] - action `finallyPy` Py [CU.exp| void { PyGILState_Release($(int gil_state)) } |] + action `finally` Py [CU.exp| void { PyGILState_Release($(int gil_state)) } |] -- | Drop GIL temporarily dropGIL :: IO a -> Py a @@ -271,7 +273,7 @@ dropGIL action = do -- | Decrement reference counter at end of ContT block takeOwnership :: Ptr PyObject -> Program r (Ptr PyObject) -takeOwnership p = ContT $ \c -> c p `finallyPy` decref p +takeOwnership p = ContT $ \c -> c p `finally` decref p -- | Wrap raw python object into @@ -353,7 +355,7 @@ checkThrowPyError :: Py () checkThrowPyError = Py [CU.exp| PyObject* { PyErr_Occurred() } |] >>= \case NULL -> pure () - _ -> throwPy =<< convertPy2Haskell + _ -> throwM =<< convertPy2Haskell -- | Throw python error as haskell exception if it's raised. If it's -- not that internal error. Another exception will be raised @@ -361,7 +363,7 @@ mustThrowPyError :: String -> Py a mustThrowPyError msg = Py [CU.exp| PyObject* { PyErr_Occurred() } |] >>= \case NULL -> error $ "mustThrowPyError: no python exception raised. " ++ msg - _ -> throwPy =<< convertPy2Haskell + _ -> throwM =<< convertPy2Haskell checkThrowBadPyType :: Py () checkThrowBadPyType = do @@ -374,7 +376,7 @@ checkThrowBadPyType = do } |] case r of 0 -> pure () - _ -> throwPy BadPyType + _ -> throwM BadPyType ---------------------------------------------------------------- diff --git a/src/Python/Internal/Program.hs b/src/Python/Internal/Program.hs index ebd845d..303eaee 100644 --- a/src/Python/Internal/Program.hs +++ b/src/Python/Internal/Program.hs @@ -16,6 +16,7 @@ module Python.Internal.Program ) where import Control.Monad.Trans.Cont +import Control.Monad.Catch import Data.Coerce import Foreign.Ptr import Foreign.Marshal.Array @@ -51,13 +52,13 @@ checkNull action = ContT $ \cnt -> action >>= \case finallyProg :: Py b -- ^ Finalizer -> Program r () -finallyProg fini = ContT $ \c -> c () `finallyPy` fini +finallyProg fini = ContT $ \c -> c () `finally` fini -- | Evaluate finalizer if exception is thrown. onExceptionProg :: Py b -- ^ Finalizer -> Program r () -onExceptionProg fini = ContT $ \c -> c () `onExceptionPy` fini +onExceptionProg fini = ContT $ \c -> c () `onException` fini ---------------------------------------------------------------- diff --git a/src/Python/Internal/Types.hs b/src/Python/Internal/Types.hs index 6d27ce4..578d59a 100644 --- a/src/Python/Internal/Types.hs +++ b/src/Python/Internal/Types.hs @@ -12,11 +12,6 @@ module Python.Internal.Types , PyThreadState , PyError(..) , Py(..) - , catchPy - , finallyPy - , onExceptionPy - , throwPy - , tryPy -- * inline-C , pyCtx -- * Patterns @@ -26,8 +21,8 @@ module Python.Internal.Types , pattern NULL ) where -import Control.Exception import Control.Monad.IO.Class +import Control.Monad.Catch import Data.Coerce import Data.Int import Data.Map.Strict qualified as Map @@ -81,23 +76,10 @@ instance Exception PyError -- It's needed in order to distinguish between code that needs such -- guarantees and plain IO. newtype Py a = Py (IO a) - deriving newtype (Functor,Applicative,Monad,MonadIO,MonadFail) + deriving newtype (Functor,Applicative,Monad,MonadIO,MonadFail, + MonadThrow,MonadCatch,MonadMask) -- See NOTE: [Python and threading] -catchPy :: forall e a. Exception e => Py a -> (e -> Py a) -> Py a -catchPy = coerce (catch @e @a) - -finallyPy :: forall a b. Py a -> Py b -> Py a -finallyPy = coerce (finally @a @b) - -onExceptionPy :: forall a b. Py a -> Py b -> Py a -onExceptionPy = coerce (onException @a @b) - -throwPy :: Exception e => e -> Py a -throwPy = Py . throwIO - -tryPy :: forall e a. Exception e => Py a -> Py (Either e a) -tryPy = coerce (try @e @a) ---------------------------------------------------------------- -- inline-C diff --git a/test/TST/Callbacks.hs b/test/TST/Callbacks.hs index 649bed4..b09e7b5 100644 --- a/test/TST/Callbacks.hs +++ b/test/TST/Callbacks.hs @@ -10,7 +10,7 @@ import TST.Util tests :: TestTree tests = testGroup "Callbacks" - [ testCase "Function(arity 0)" $ do + [ testCase "Function(arity 0)" $ runPy $ do let double = pure 2 :: IO Int [py_| # OK @@ -21,7 +21,7 @@ tests = testGroup "Callbacks" except TypeError as e: pass |] - , testCase "Function(arity=1)" $ do + , testCase "Function(arity=1)" $ runPy $ do let double = pure . (*2) :: Int -> IO Int [py_| # OK @@ -37,7 +37,7 @@ tests = testGroup "Callbacks" except TypeError as e: pass |] - , testCase "Function(arity=2)" $ do + , testCase "Function(arity=2)" $ runPy $ do let foo :: Int -> Double -> IO Int foo x y = pure $ x + round y [py_| @@ -54,31 +54,31 @@ tests = testGroup "Callbacks" except TypeError as e: pass |] - , testCase "Haskell exception in callback(arity=1)" $ do + , testCase "Haskell exception in callback(arity=1)" $ runPy $ do let foo :: Int -> IO Int foo y = pure $ 10 `div` y throwsPy [py_| foo_hs(0) |] - , testCase "Haskell exception in callback(arity=2)" $ do + , testCase "Haskell exception in callback(arity=2)" $ runPy $ do let foo :: Int -> Int -> IO Int foo x y = pure $ x `div` y throwsPy [py_| foo_hs(1, 0) |] ---------------------------------------- - , testCase "Call python in callback (arity=1)" $ do + , testCase "Call python in callback (arity=1)" $ runPy $ do let foo :: Int -> IO Int - foo x = do Just x' <- fromPy =<< [pye| 100 // x_hs |] + foo x = do Just x' <- runPy $ fromPy =<< [pye| 100 // x_hs |] pure x' [py_| assert foo_hs(5) == 20 |] - , testCase "Call python in callback (arity=2" $ do + , testCase "Call python in callback (arity=2" $ runPy $ do let foo :: Int -> Int -> IO Int - foo x y = do Just x' <- fromPy =<< [pye| x_hs // y_hs |] + foo x y = do Just x' <- runPy $ fromPy =<< [pye| x_hs // y_hs |] pure x' [py_| assert foo_hs(100,5) == 20 |] ---------------------------------------- - , testCase "No leaks (arity=1)" $ do + , testCase "No leaks (arity=1)" $ runPy $ do let foo :: Int -> IO Int foo y = pure $ 10 * y [py_| @@ -88,7 +88,7 @@ tests = testGroup "Callbacks" foo_hs(x) assert old_refcount == sys.getrefcount(x) |] - , testCase "No leaks (arity=2)" $ do + , testCase "No leaks (arity=2)" $ runPy $ do let foo :: Int -> Int -> IO Int foo x y = pure $ x * y [py_| diff --git a/test/TST/FromPy.hs b/test/TST/FromPy.hs index ca594c2..5f3279b 100644 --- a/test/TST/FromPy.hs +++ b/test/TST/FromPy.hs @@ -2,6 +2,7 @@ -- | module TST.FromPy (tests) where +import Control.Monad.IO.Class import Test.Tasty import Test.Tasty.HUnit import Python.Inline @@ -10,32 +11,32 @@ import Python.Inline.QQ tests :: TestTree tests = testGroup "FromPy" [ testGroup "Int" - [ testCase "Int->Int" $ eq @Int (Just 1234) =<< [pye| 1234 |] - , testCase "Double->Int" $ eq @Int Nothing =<< [pye| 1234.25 |] - , testCase "None->Int" $ eq @Int Nothing =<< [pye| None |] + [ testCase "Int->Int" $ eq @Int (Just 1234) [pye| 1234 |] + , testCase "Double->Int" $ eq @Int Nothing [pye| 1234.25 |] + , testCase "None->Int" $ eq @Int Nothing [pye| None |] ] , testGroup "Double" - [ testCase "Int->Double" $ eq @Double (Just 1234) =<< [pye| 1234 |] - , testCase "Double->Double" $ eq @Double (Just 1234.25) =<< [pye| 1234.25 |] - , testCase "None->Double" $ eq @Double Nothing =<< [pye| None |] + [ testCase "Int->Double" $ eq @Double (Just 1234) [pye| 1234 |] + , testCase "Double->Double" $ eq @Double (Just 1234.25) [pye| 1234.25 |] + , testCase "None->Double" $ eq @Double Nothing [pye| None |] ] , testGroup "Char" - [ testCase "0" $ eq @Char Nothing =<< [pye| "" |] - , testCase "1 1B" $ eq @Char (Just 'a') =<< [pye| "a" |] - , testCase "2 2B" $ eq @Char (Just 'ы') =<< [pye| "ы" |] - , testCase "2" $ eq @Char Nothing =<< [pye| "as" |] - , testCase "None" $ eq @Char Nothing =<< [pye| None |] + [ testCase "0" $ eq @Char Nothing [pye| "" |] + , testCase "1 1B" $ eq @Char (Just 'a') [pye| "a" |] + , testCase "2 2B" $ eq @Char (Just 'ы') [pye| "ы" |] + , testCase "2" $ eq @Char Nothing [pye| "as" |] + , testCase "None" $ eq @Char Nothing [pye| None |] ] , testGroup "String" - [ testCase "asdf" $ eq @String (Just "asdf") =<< [pye| "asdf" |] - , testCase "фыва" $ eq @String (Just "фыва") =<< [pye| "фыва" |] + [ testCase "asdf" $ eq @String (Just "asdf") [pye| "asdf" |] + , testCase "фыва" $ eq @String (Just "фыва") [pye| "фыва" |] ] , testGroup "Bool" - [ testCase "True->Bool" $ eq @Bool (Just True) =<< [pye| True |] - , testCase "False->Bool" $ eq @Bool (Just False) =<< [pye| False |] - , testCase "None->Bool" $ eq @Bool (Just False) =<< [pye| None |] + [ testCase "True->Bool" $ eq @Bool (Just True) [pye| True |] + , testCase "False->Bool" $ eq @Bool (Just False) [pye| False |] + , testCase "None->Bool" $ eq @Bool (Just False) [pye| None |] -- FIXME: Names defined in pymain leak! - , testCase "Exception" $ do + , testCase "Exception" $ runPy $ do [pymain| class Bad: def __bool__(self): @@ -46,39 +47,39 @@ tests = testGroup "FromPy" [py_| 1+1 |] ] , testGroup "Tuple2" - [ testCase "T2" $ eq @(Int,Bool) (Just (2,True)) =<< [pye| (2,3) |] - , testCase "L2" $ eq @(Int,Bool) (Just (2,True)) =<< [pye| [2,3] |] - , testCase "L1" $ eq @(Int,Bool) Nothing =<< [pye| [1] |] - , testCase "T3" $ eq @(Int,Bool) Nothing =<< [pye| (1,2,3) |] - , testCase "X" $ eq @(Int,Bool) Nothing =<< [pye| 2 |] + [ testCase "T2" $ eq @(Int,Bool) (Just (2,True)) [pye| (2,3) |] + , testCase "L2" $ eq @(Int,Bool) (Just (2,True)) [pye| [2,3] |] + , testCase "L1" $ eq @(Int,Bool) Nothing [pye| [1] |] + , testCase "T3" $ eq @(Int,Bool) Nothing [pye| (1,2,3) |] + , testCase "X" $ eq @(Int,Bool) Nothing [pye| 2 |] ] , testGroup "Tuple3" - [ testCase "T3" $ eq @(Int,Int,Int) (Just (1,2,3)) =<< [pye| (1,2,3) |] - , testCase "L3" $ eq @(Int,Int,Int) (Just (1,2,3)) =<< [pye| [1,2,3] |] - , testCase "L1" $ eq @(Int,Int,Int) Nothing =<< [pye| [1] |] - , testCase "T4" $ eq @(Int,Int,Int) Nothing =<< [pye| (1,2,3,4) |] - , testCase "X" $ eq @(Int,Int,Int) Nothing =<< [pye| 2 |] + [ testCase "T3" $ eq @(Int,Int,Int) (Just (1,2,3)) [pye| (1,2,3) |] + , testCase "L3" $ eq @(Int,Int,Int) (Just (1,2,3)) [pye| [1,2,3] |] + , testCase "L1" $ eq @(Int,Int,Int) Nothing [pye| [1] |] + , testCase "T4" $ eq @(Int,Int,Int) Nothing [pye| (1,2,3,4) |] + , testCase "X" $ eq @(Int,Int,Int) Nothing [pye| 2 |] ] , testGroup "Tuple4" - [ testCase "T4" $ eq @(Int,Int,Int,Int) (Just (1,2,3,4)) =<< [pye| (1,2,3,4) |] - , testCase "L4" $ eq @(Int,Int,Int,Int) (Just (1,2,3,4)) =<< [pye| [1,2,3,4] |] - , testCase "L1" $ eq @(Int,Int,Int,Int) Nothing =<< [pye| [1] |] - , testCase "X" $ eq @(Int,Int,Int,Int) Nothing =<< [pye| 2 |] + [ testCase "T4" $ eq @(Int,Int,Int,Int) (Just (1,2,3,4)) [pye| (1,2,3,4) |] + , testCase "L4" $ eq @(Int,Int,Int,Int) (Just (1,2,3,4)) [pye| [1,2,3,4] |] + , testCase "L1" $ eq @(Int,Int,Int,Int) Nothing [pye| [1] |] + , testCase "X" $ eq @(Int,Int,Int,Int) Nothing [pye| 2 |] ] , testGroup "List" - [ testCase "()" $ eq @[Int] (Just []) =<< [pye| () |] - , testCase "[]" $ eq @[Int] (Just []) =<< [pye| [] |] - , testCase "[1]" $ eq @[Int] (Just [1]) =<< [pye| [1] |] - , testCase "[3]" $ eq @[Int] (Just [1,2,3]) =<< [pye| [1,2,3] |] - , testCase "Int" $ eq @[Int] Nothing =<< [pye| None |] + [ testCase "()" $ eq @[Int] (Just []) [pye| () |] + , testCase "[]" $ eq @[Int] (Just []) [pye| [] |] + , testCase "[1]" $ eq @[Int] (Just [1]) [pye| [1] |] + , testCase "[3]" $ eq @[Int] (Just [1,2,3]) [pye| [1,2,3] |] + , testCase "Int" $ eq @[Int] Nothing [pye| None |] ] ] -eq :: (Eq a, Show a, FromPy a) => Maybe a -> PyObject -> IO () -eq a p = assertEqual "fromPy: " a =<< fromPy p +eq :: (Eq a, Show a, FromPy a) => Maybe a -> (Py PyObject) -> IO () +eq a action = assertEqual "fromPy: " a =<< runPy (fromPy =<< action) -failE :: forall a. (Eq a, Show a, FromPy a) => PyObject -> IO () +failE :: forall a. (Eq a, Show a, FromPy a) => PyObject -> Py () failE p = fromPyEither @a p >>= \case Left PyError{} -> pure () - r -> assertFailure $ "Should fail with exception, but: " ++ show r + r -> liftIO $ assertFailure $ "Should fail with exception, but: " ++ show r diff --git a/test/TST/Roundtrip.hs b/test/TST/Roundtrip.hs index 6320e81..596e1fe 100644 --- a/test/TST/Roundtrip.hs +++ b/test/TST/Roundtrip.hs @@ -78,7 +78,7 @@ testOutOfRange = testProperty propRoundtrip :: forall a. (FromPy a, ToPy a, Eq a) => a -> Property propRoundtrip a = ioProperty $ do - a' <- fromPy' =<< [pye| a_hs |] + a' <- runPy $ fromPy' =<< [pye| a_hs |] pure $ a == a' @@ -87,7 +87,7 @@ propOutOfRange :: forall a wide. (ToPy wide, FromPy a, Eq a, Eq wide, Integral wide, Integral a) => wide -> Property propOutOfRange wide = ioProperty $ do - a_py <- fromPy @a =<< [pye| wide_hs |] + a_py <- runPy $ fromPy @a =<< [pye| wide_hs |] pure $ a_hs == a_py where -- Convert taking range into account diff --git a/test/TST/Run.hs b/test/TST/Run.hs index d0b0e08..c468e0c 100644 --- a/test/TST/Run.hs +++ b/test/TST/Run.hs @@ -11,10 +11,10 @@ import TST.Util tests :: TestTree tests = testGroup "Run python" - [ testCase "Empty QQ" [py_| |] - , testCase "Second init is noop" initializePython - , testCase "Python exceptions are converted" $ throwsPy [py_| 1 / 0 |] - , testCase "Scope pymain->any" $ do + [ testCase "Empty QQ" $ runPy [py_| |] + , testCase "Second init is noop" $ initializePython + , testCase "Python exceptions are converted" $ runPy $ throwsPy [py_| 1 / 0 |] + , testCase "Scope pymain->any" $ runPy $ do [pymain| x = 12 x @@ -41,7 +41,7 @@ tests = testGroup "Run python" except NameError: pass |] - , testCase "Scope py_->any" $ do + , testCase "Scope py_->any" $ runPy $ do [py_| x = 12 x @@ -62,7 +62,7 @@ tests = testGroup "Run python" except NameError: pass |] - , testCase "Import py_->any" $ do + , testCase "Import py_->any" $ runPy $ do [py_| import sys sys @@ -83,7 +83,7 @@ tests = testGroup "Run python" except NameError: pass |] - , testCase "Scope pyf->any" $ do + , testCase "Scope pyf->any" $ runPy $ do _ <- [pyf| x = 12 x diff --git a/test/TST/ToPy.hs b/test/TST/ToPy.hs index 28f8d03..aba6800 100644 --- a/test/TST/ToPy.hs +++ b/test/TST/ToPy.hs @@ -3,30 +3,31 @@ module TST.ToPy (tests) where import Test.Tasty import Test.Tasty.HUnit +import Python.Inline import Python.Inline.QQ tests :: TestTree tests = testGroup "ToPy" - [ testCase "Int" $ let i = 1234 :: Int in [py_| assert i_hs == 1234 |] - , testCase "Double" $ let i = 1234.25 :: Double in [py_| assert i_hs == 1234.25 |] - , testCase "Char ASCII" $ let c = 'a' in [py_| assert c_hs == 'a' |] - , testCase "Char unicode" $ let c = 'ы' in [py_| assert c_hs == 'ы' |] - , testCase "String ASCII" $ let c = "asdf" in [py_| assert c_hs == 'asdf' |] - , testCase "String unicode" $ let c = "фыва" in [py_| assert c_hs == 'фыва' |] + [ testCase "Int" $ runPy $ let i = 1234 :: Int in [py_| assert i_hs == 1234 |] + , testCase "Double" $ runPy $ let i = 1234.25 :: Double in [py_| assert i_hs == 1234.25 |] + , testCase "Char ASCII" $ runPy $ let c = 'a' in [py_| assert c_hs == 'a' |] + , testCase "Char unicode" $ runPy $ let c = 'ы' in [py_| assert c_hs == 'ы' |] + , testCase "String ASCII" $ runPy $ let c = "asdf" in [py_| assert c_hs == 'asdf' |] + , testCase "String unicode" $ runPy $ let c = "фыва" in [py_| assert c_hs == 'фыва' |] -- Container types - , testCase "Tuple2" $ + , testCase "Tuple2" $ runPy $ let x = (1::Int, 333::Int) in [py_| assert x_hs == (1,333) |] - , testCase "Tuple3" $ + , testCase "Tuple3" $ runPy $ let x = (1::Int, 333::Int, True) in [py_| assert x_hs == (1,333,True) |] - , testCase "Tuple4" $ + , testCase "Tuple4" $ runPy $ let x = (1::Int, 333::Int, True, 'c') in [py_| assert x_hs == (1,333,True,'c') |] - , testCase "nested Tuple2" $ + , testCase "nested Tuple2" $ runPy $ let x = (1::Int, (333::Int,4.5::Double)) in [py_| assert x_hs == (1,(333,4.5)) |] - , testCase "list" $ + , testCase "list" $ runPy $ let x = [1 .. 5::Int] in [py_| assert x_hs == [1,2,3,4,5] |] ] diff --git a/test/TST/Util.hs b/test/TST/Util.hs index c6f6cb5..dad133f 100644 --- a/test/TST/Util.hs +++ b/test/TST/Util.hs @@ -1,12 +1,13 @@ -- | module TST.Util where -import Control.Exception +import Control.Monad.IO.Class +import Control.Monad.Catch import Test.Tasty.HUnit import Python.Inline -throwsPy :: IO () -> IO () -throwsPy io = (io >> assertFailure "Evaluation should raise python exception") +throwsPy :: Py () -> Py () +throwsPy io = (io >> liftIO (assertFailure "Evaluation should raise python exception")) `catch` (\(_::PyError) -> pure ())