From bc067e9ffe100ed1ba896c0bedc76bcabe5b3ccb Mon Sep 17 00:00:00 2001 From: Alexey Khudyakov Date: Tue, 7 Jan 2025 17:13:48 +0300 Subject: [PATCH 1/3] Add isInitialized --- src/Python/Internal/Eval.hs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/Python/Internal/Eval.hs b/src/Python/Internal/Eval.hs index 98d6b57..3cacd61 100644 --- a/src/Python/Internal/Eval.hs +++ b/src/Python/Internal/Eval.hs @@ -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 From 91eed63d7c059c3752f612c61a9652806ef847c3 Mon Sep 17 00:00:00 2001 From: Alexey Khudyakov Date: Wed, 8 Jan 2025 01:53:33 +0300 Subject: [PATCH 2/3] Add Monad{Throw,Mask,Catch} instances for Py --- inline-python.cabal | 1 + src/Python/Inline/Literal.hs | 39 +++++++++++++++++----------------- src/Python/Internal/Eval.hs | 12 +++++------ src/Python/Internal/Program.hs | 5 +++-- src/Python/Internal/Types.hs | 24 +++------------------ 5 files changed, 33 insertions(+), 48 deletions(-) diff --git a/inline-python.cabal b/inline-python.cabal index 8c48018..1c1e68e 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 diff --git a/src/Python/Inline/Literal.hs b/src/Python/Inline/Literal.hs index cec1292..62efd48 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 @@ -83,7 +84,7 @@ class FromPy a where -- 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) + (Right <$> basicFromPy p) `catch` (pure . Left) -- | Convert python object to haskell value. Will return @Nothing@ if @@ -91,10 +92,10 @@ fromPyEither py = runPy $ unsafeWithPyObject py $ \p -> -- are rethrown. fromPy :: FromPy a => PyObject -> IO (Maybe a) fromPy py = runPy $ unsafeWithPyObject py $ \p -> - (Just <$> basicFromPy p) `catchPy` \case + (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. @@ -104,7 +105,7 @@ fromPy' py = runPy $ 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 + 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/Internal/Eval.hs b/src/Python/Internal/Eval.hs index 3cacd61..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 @@ -260,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 @@ -273,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 @@ -355,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 @@ -363,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 @@ -376,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 From 6ff14e957c62a9c8c7cab09340f11e186e879588 Mon Sep 17 00:00:00 2001 From: Alexey Khudyakov Date: Wed, 8 Jan 2025 02:13:58 +0300 Subject: [PATCH 3/3] Perform all work with python in Py Executing everything in single atomic block is only way to maintain coherency when interaction with huge pile of mutable state which is python. So let move everything from QQ to conversion to Py --- bench/Main.hs | 5 +-- inline-python.cabal | 1 + src/Python/Inline.hs | 2 + src/Python/Inline/Literal.hs | 16 +++---- src/Python/Inline/QQ.hs | 14 +++---- test/TST/Callbacks.hs | 22 +++++----- test/TST/FromPy.hs | 81 ++++++++++++++++++------------------ test/TST/Roundtrip.hs | 4 +- test/TST/Run.hs | 14 +++---- test/TST/ToPy.hs | 23 +++++----- test/TST/Util.hs | 7 ++-- 11 files changed, 97 insertions(+), 92 deletions(-) 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 1c1e68e..3f6bba4 100644 --- a/inline-python.cabal +++ b/inline-python.cabal @@ -85,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 62efd48..f314347 100644 --- a/src/Python/Inline/Literal.hs +++ b/src/Python/Inline/Literal.hs @@ -82,16 +82,16 @@ 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 -> +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 -> +fromPy :: FromPy a => PyObject -> Py (Maybe a) +fromPy py = unsafeWithPyObject py $ \p -> (Just <$> basicFromPy p) `catch` \case BadPyType -> pure Nothing OutOfRange -> pure Nothing @@ -99,12 +99,12 @@ fromPy py = runPy $ unsafeWithPyObject py $ \p -> -- | 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 +toPy :: ToPy a => a -> Py PyObject +toPy a = basicToPy a >>= \case NULL -> throwM =<< convertPy2Haskell p -> newPyObject p 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/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 ())