inline-python 0.1.1.1 → 0.2
raw patch · 14 files changed
+636/−126 lines, 14 filesdep ~bytestringdep ~text
Dependency ranges changed: bytestring, text
Files
- ChangeLog.md +8/−0
- inline-python.cabal +5/−2
- src/Python/Inline/Eval.hs +59/−0
- src/Python/Inline/Literal.hs +171/−1
- src/Python/Inline/QQ.hs +22/−3
- src/Python/Internal/Eval.hs +172/−8
- src/Python/Internal/EvalQQ.hs +37/−103
- src/Python/Internal/Program.hs +4/−0
- src/Python/Internal/Types.hs +70/−1
- test/TST/FromPy.hs +19/−3
- test/TST/Roundtrip.hs +19/−0
- test/TST/Run.hs +33/−0
- test/TST/ToPy.hs +15/−4
- test/TST/Util.hs +2/−1
ChangeLog.md view
@@ -1,3 +1,11 @@+0.2 [2025.05.04]+----------------+* `FromPy`/`ToPy` instances added for: `Complex`, both strict and lazy `Text` &+ `ByteString`, `ShortByteString`, `Maybe a`+* Module `Python.Inline.Eval` added which support for eval/exec with user+ supplied global and local variables.+* QuasiQuotes `Python.Inline.QQ.pycode` added for creating `PyQuote` data type.+ 0.1.1.1 [2025.03.10] -------------------- * Crash of python's main thread when one attempts to interrupt it fixed.
inline-python.cabal view
@@ -2,7 +2,7 @@ Build-Type: Simple Name: inline-python-Version: 0.1.1.1+Version: 0.2 Synopsis: Python interpreter embedded into haskell. Description: This package embeds python interpreter into haskell program and@@ -57,7 +57,7 @@ , stm >=2.4 , template-haskell -any , text >=2- , bytestring+ , bytestring >=0.11.2 , exceptions >=0.10 , vector >=0.13 hs-source-dirs: src@@ -70,6 +70,7 @@ Python.Inline Python.Inline.Literal Python.Inline.QQ+ Python.Inline.Eval Python.Inline.Types Other-modules: Python.Internal.CAPI@@ -93,6 +94,8 @@ , exceptions , containers , vector+ , bytestring+ , text hs-source-dirs: test Exposed-modules: TST.Run
+ src/Python/Inline/Eval.hs view
@@ -0,0 +1,59 @@+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TemplateHaskell #-}+-- |+-- Interface to python's @eval@ and @exec@ which gives programmer+-- control over local and global variables.+module Python.Inline.Eval+ ( -- * Python execution+ eval+ , exec+ -- * Source code+ , PyQuote(..)+ , Code+ , codeFromText+ , codeFromString+ , DictBinder+ , bindVar+ -- * Variable namespaces+ , Namespace(..)+ , Main(..)+ , Temp(..)+ , Dict(..)+ , Module(..)+ ) where++import Data.ByteString.Unsafe qualified as BS+import Data.Text (Text)+import Data.Text.Encoding qualified as T+import Language.C.Inline qualified as C+import Language.C.Inline.Unsafe qualified as CU++import Python.Internal.Types+import Python.Internal.Eval+import Python.Internal.Program+import Python.Inline.Literal+++----------------------------------------------------------------+C.context (C.baseCtx <> pyCtx)+C.include "<inline-python.h>"+----------------------------------------------------------------++-- | Bind variable in dictionary+bindVar+ :: (ToPy a)+ => Text -- ^ Variable name+ -> a -- ^ Variable value+ -> DictBinder+bindVar name a = DictBinder $ \p_dict -> runProgram $ do+ p_key <- progIOBracket $ BS.unsafeUseAsCString (T.encodeUtf8 name)+ p_obj <- takeOwnership =<< progPy (throwOnNULL =<< basicToPy a)+ progPy $ do+ r <- Py [CU.block| int {+ PyObject* p_obj = $(PyObject* p_obj);+ return PyDict_SetItemString($(PyObject* p_dict), $(char* p_key), p_obj);+ } |]+ case r of+ 0 -> pure ()+ _ -> mustThrowPyError+
src/Python/Inline/Literal.hs view
@@ -1,5 +1,6 @@ {-# LANGUAGE CPP #-} {-# LANGUAGE ForeignFunctionInterface #-}+{-# LANGUAGE MagicHash #-} {-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE TemplateHaskell #-} -- |@@ -13,6 +14,7 @@ , fromPy' ) where +import Control.Exception (evaluate) import Control.Monad import Control.Monad.Catch import Control.Monad.Trans.Cont@@ -20,8 +22,15 @@ import Data.Char import Data.Int import Data.Word+import Data.ByteString qualified as BS+import Data.ByteString.Unsafe qualified as BS+import Data.ByteString.Short qualified as SBS+import Data.ByteString.Lazy qualified as BL import Data.Set qualified as Set import Data.Map.Strict qualified as Map+import Data.Text qualified as T+import Data.Text.Encoding qualified as T+import Data.Text.Lazy qualified as TL import Data.Vector.Generic qualified as VG import Data.Vector.Generic.Mutable qualified as MVG import Data.Vector qualified as V@@ -34,7 +43,10 @@ import Foreign.Ptr import Foreign.C.Types import Foreign.Storable+import Foreign.Marshal.Alloc (alloca,mallocBytes)+import Foreign.Marshal.Utils (copyBytes) import GHC.Float (float2Double, double2Float)+import Data.Complex (Complex((:+))) import Language.C.Inline qualified as C import Language.C.Inline.Unsafe qualified as CU@@ -127,6 +139,11 @@ instance FromPy PyObject where basicFromPy p = incref p >> newPyObject p +deriving newtype instance ToPy Module+deriving newtype instance FromPy Module+deriving newtype instance ToPy Dict+deriving newtype instance FromPy Dict+ instance ToPy () where basicToPy () = Py [CU.exp| PyObject* { Py_None } |] @@ -191,7 +208,29 @@ instance ToPy Float where basicToPy = basicToPy . float2Double instance FromPy Float where basicFromPy = fmap double2Float . basicFromPy +-- | @since 0.2+instance ToPy (Complex Float) where+ basicToPy (x:+y) = basicToPy $ float2Double x :+ float2Double y+-- | @since 0.2+instance FromPy (Complex Float) where+ basicFromPy xy_py = do+ x :+ y <- basicFromPy xy_py+ return $ double2Float x :+ double2Float y +-- | @since 0.2+instance ToPy (Complex Double) where+ basicToPy (x:+y) = Py [CU.exp| PyObject* { PyComplex_FromDoubles($(double x'), $(double y')) } |]+ where x' = CDouble x+ y' = CDouble y+-- | @since 0.2+instance FromPy (Complex Double) where+ basicFromPy xy_py = do+ CDouble x <- Py [CU.exp| double { PyComplex_RealAsDouble($(PyObject *xy_py)) } |]+ checkThrowBadPyType+ CDouble y <- Py [CU.exp| double { PyComplex_ImagAsDouble($(PyObject *xy_py)) } |]+ checkThrowBadPyType+ return $ x :+ y+ instance ToPy Int where basicToPy | wordSizeInBits == 64 = basicToPy @Int64 . fromIntegral@@ -377,6 +416,24 @@ d <- basicFromPy p_d pure (a,b,c,d) ++-- | @Nothing@ is encoded as @None@. @Just a@ same as @a@.+--+-- @since 0.2+instance (ToPy a) => ToPy (Maybe a) where+ basicToPy Nothing = Py [CU.exp| PyObject* { Py_None } |]+ basicToPy (Just a) = basicToPy a++-- | @None@ is decoded as @Nothing@ rest is attempted to be decoded as @a@+--+-- @since 0.2+instance (FromPy a) => FromPy (Maybe a) where+ basicFromPy p =+ Py [CU.exp| bool { Py_None == $(PyObject *p) } |] >>= \case+ 0 -> Just <$> basicFromPy p+ _ -> pure Nothing++ instance (ToPy a) => ToPy [a] where basicToPy = basicListToPy @@ -483,7 +540,7 @@ #endif --- | Fold over iterable. Function takes ownership over iterator.+-- | Fold over python's iterator. Function takes ownership over iterator. foldPyIterable :: Ptr PyObject -- ^ Python iterator (not checked) -> (a -> Ptr PyObject -> Py a) -- ^ Step function. It takes borrowed pointer.@@ -529,6 +586,119 @@ where n = VG.length vec n_c = fromIntegral n :: CLLong+++-- | Converted to @bytes@+--+-- @since 0.2+instance ToPy BS.ByteString where+ basicToPy bs = pyIO $ BS.unsafeUseAsCStringLen bs $ \(ptr,len) -> do+ let c_len = fromIntegral len :: CLLong+ py <- [CU.exp| PyObject* { PyBytes_FromStringAndSize($(char* ptr), $(long long c_len)) }|]+ case py of+ NULL -> unsafeRunPy mustThrowPyError+ _ -> return py++-- | Accepts @bytes@ and @bytearray@+--+-- @since 0.2+instance FromPy BS.ByteString where+ basicFromPy py = pyIO $ do+ [CU.exp| int { PyBytes_Check($(PyObject* py)) } |] >>= \case+ TRUE -> do+ sz <- [CU.exp| int64_t { PyBytes_GET_SIZE( $(PyObject* py)) } |]+ buf <- [CU.exp| char* { PyBytes_AS_STRING($(PyObject* py)) } |]+ fini buf (fromIntegral sz)+ _ -> [CU.exp| int { PyByteArray_Check($(PyObject* py)) } |] >>= \case+ TRUE -> do+ sz <- [CU.exp| int64_t { PyByteArray_GET_SIZE( $(PyObject* py)) } |]+ buf <- [CU.exp| char* { PyByteArray_AS_STRING($(PyObject* py)) } |]+ fini buf (fromIntegral sz)+ _ -> throwM BadPyType+ where+ fini py_buf sz = do+ hs_buf <- mallocBytes sz+ copyBytes hs_buf py_buf sz+ BS.unsafePackMallocCStringLen (hs_buf, sz)++-- | Converted to @bytes@+--+-- @since 0.2+instance ToPy BL.ByteString where+ basicToPy = basicToPy . BL.toStrict++-- | Accepts @bytes@ and @bytearray@+--+-- @since 0.2+instance FromPy BL.ByteString where+ basicFromPy = fmap BL.fromStrict . basicFromPy+++-- | Accepts @bytes@ and @bytearray@+--+-- @since 0.2+instance FromPy SBS.ShortByteString where+ basicFromPy py = pyIO $ do+ [CU.exp| int { PyBytes_Check($(PyObject* py)) } |] >>= \case+ TRUE -> do+ sz <- [CU.exp| int64_t { PyBytes_GET_SIZE( $(PyObject* py)) } |]+ buf <- [CU.exp| char* { PyBytes_AS_STRING($(PyObject* py)) } |]+ fini buf (fromIntegral sz)+ _ -> [CU.exp| int { PyByteArray_Check($(PyObject* py)) } |] >>= \case+ TRUE -> do+ sz <- [CU.exp| int64_t { PyByteArray_GET_SIZE( $(PyObject* py)) } |]+ buf <- [CU.exp| char* { PyByteArray_AS_STRING($(PyObject* py)) } |]+ fini buf (fromIntegral sz)+ _ -> throwM BadPyType+ where+ fini buf sz = do+ bs <- BS.unsafePackCStringLen (buf, sz)+ evaluate $ SBS.toShort bs++-- | Converted to @bytes@+--+-- @since 0.2+instance ToPy SBS.ShortByteString where+ basicToPy bs = pyIO $ SBS.useAsCStringLen bs $ \(ptr,len) -> do+ let c_len = fromIntegral len :: CLLong+ py <- [CU.exp| PyObject* { PyBytes_FromStringAndSize($(char* ptr), $(long long c_len)) }|]+ case py of+ NULL -> unsafeRunPy mustThrowPyError+ _ -> return py+++-- | @since 0.2@.+instance ToPy T.Text where+ -- NOTE: Is there ore efficient way to access+ basicToPy str = pyIO $ BS.unsafeUseAsCStringLen bs $ \(ptr,len) -> do+ let c_len = fromIntegral len :: CLLong+ py <- [CU.exp| PyObject* { PyUnicode_FromStringAndSize($(char* ptr), $(long long c_len)) } |]+ case py of+ NULL -> unsafeRunPy mustThrowPyError+ _ -> pure py+ where+ bs = T.encodeUtf8 str++-- | @since 0.2@.+instance ToPy TL.Text where+ basicToPy = basicToPy . TL.toStrict++-- | @since 0.2@.+instance FromPy T.Text where+ basicFromPy py = pyIO $ do+ [CU.exp| int { PyUnicode_Check($(PyObject* py)) } |] >>= \case+ TRUE -> alloca $ \p_size -> do+ buf <- [CU.exp| const char* { PyUnicode_AsUTF8AndSize($(PyObject* py), $(long* p_size)) } |]+ sz <- peek p_size+ bs <- BS.unsafePackCStringLen (buf, fromIntegral sz)+ return $! T.decodeUtf8Lenient bs+ _ -> throwM BadPyType++-- | @since 0.2@.+instance FromPy TL.Text where+ basicFromPy = fmap TL.fromStrict . basicFromPy++ ---------------------------------------------------------------- -- Functions marshalling
src/Python/Inline/QQ.hs view
@@ -29,16 +29,21 @@ -- > do_this() -- > do_that() -- > |]+--+-- If control over python's global and local variables is+-- required. APIs from "Python.Inline.Eval" should be used instead. module Python.Inline.QQ ( pymain , py_ , pye , pyf+ , pycode ) where import Language.Haskell.TH.Quote import Python.Internal.EvalQQ+import Python.Internal.Eval -- | Evaluate sequence of python statements. It works in the same way@@ -48,7 +53,7 @@ -- It creates value of type @Py ()@ pymain :: QuasiQuoter pymain = QuasiQuoter- { quoteExp = \txt -> [| evaluatorPymain $(expQQ Exec txt) |]+ { quoteExp = \txt -> [| exec Main Main $(expQQ Exec txt) |] , quotePat = error "quotePat" , quoteType = error "quoteType" , quoteDec = error "quoteDec"@@ -61,7 +66,7 @@ -- It creates value of type @Py ()@ py_ :: QuasiQuoter py_ = QuasiQuoter- { quoteExp = \txt -> [| evaluatorPy_ $(expQQ Exec txt) |]+ { quoteExp = \txt -> [| exec Main Temp $(expQQ Exec txt) |] , quotePat = error "quotePat" , quoteType = error "quoteType" , quoteDec = error "quoteDec"@@ -73,7 +78,7 @@ -- This quote creates object of type @Py PyObject@ pye :: QuasiQuoter pye = QuasiQuoter- { quoteExp = \txt -> [| evaluatorPye $(expQQ Eval txt) |]+ { quoteExp = \txt -> [| eval Main Temp $(expQQ Eval txt) |] , quotePat = error "quotePat" , quoteType = error "quoteType" , quoteDec = error "quoteDec"@@ -86,6 +91,20 @@ pyf :: QuasiQuoter pyf = QuasiQuoter { quoteExp = \txt -> [| evaluatorPyf $(expQQ Fun txt) |]+ , quotePat = error "quotePat"+ , quoteType = error "quoteType"+ , quoteDec = error "quoteDec"+ }++-- | Create quote of python code suitable for use with+-- 'Python.Inline.Eval.exec'+--+-- It creates value of type @PyQuote@+--+-- @since 0.2@+pycode :: QuasiQuoter+pycode = QuasiQuoter+ { quoteExp = \txt -> expQQ Exec txt , quotePat = error "quotePat" , quoteType = error "quoteType" , quoteDec = error "quoteDec"
src/Python/Internal/Eval.hs view
@@ -29,6 +29,17 @@ , mustThrowPyError , checkThrowBadPyType , throwOnNULL+ -- * Exec & eval+ , Namespace(..)+ , Main(..)+ , Temp(..)+ , Dict(..)+ , DictPtr(..)+ , Module(..)+ , ModulePtr(..)+ , unsafeWithCode+ , eval+ , exec -- * Debugging , debugPrintPy ) where@@ -42,6 +53,7 @@ import Control.Monad.Trans.Cont import Data.Maybe import Data.Function+import Data.ByteString.Unsafe qualified as BS import Foreign.Concurrent qualified as GHC import Foreign.Ptr import Foreign.ForeignPtr@@ -292,10 +304,10 @@ Py_Finalize(); } |] -- We need to call Py_Finalize on main thread- RunningN _ eval _ tid_gc -> checkLock $ do+ RunningN _ lock_eval _ tid_gc -> checkLock $ do killThread tid_gc resp <- newEmptyMVar- putMVar eval $ StopReq resp+ putMVar lock_eval $ StopReq resp takeMVar resp where checkLock action = readTVar globalPyLock >>= \case@@ -472,7 +484,7 @@ InInitialization -> retry InFinalization -> retry Running1 -> throwSTM $ PyInternalError "runPyInMain: Running1"- RunningN _ eval tid_main _ -> readTVar globalPyLock >>= \case+ RunningN _ eval_lock tid_main _ -> readTVar globalPyLock >>= \case LockUninialized -> throwSTM PythonNotInitialized LockFinalized -> throwSTM PythonIsFinalized LockedByGC -> retry@@ -480,9 +492,9 @@ LockUnlocked -> do writeTVar globalPyLock $ Locked tid_main [] pure ( atomically (releaseLock tid_main)- , evalInOtherThread tid_main eval+ , evalInOtherThread tid_main eval_lock )- -- If we can grab lock and main thread taken lock we're+ -- If we513 can grab lock and main thread taken lock we're -- already executing on main thread. We can simply execute code Locked t ts | t /= tid@@ -495,12 +507,12 @@ | otherwise -> do writeTVar globalPyLock $ Locked tid_main (t : ts) pure ( atomically (releaseLock tid_main)- , evalInOtherThread tid_main eval+ , evalInOtherThread tid_main eval_lock ) --- evalInOtherThread tid_main eval = do+ evalInOtherThread tid_main eval_lock = do r <- mask_ $ do resp <- newEmptyMVar- putMVar eval $ EvalReq py resp+ putMVar eval_lock $ EvalReq py resp takeMVar resp `onException` throwTo tid_main InterruptMain either throwM pure r @@ -660,6 +672,158 @@ case r of 0 -> pure () _ -> throwM BadPyType+++----------------------------------------------------------------+-- Eval/exec+----------------------------------------------------------------++-- | Type class for values representing python dictionaries containing+-- global or local variables.+--+-- @since 0.2@+class Namespace a where+ -- | Returns dictionary object. Caller should take ownership of+ -- returned object.+ basicNamespaceDict :: a -> Py (Ptr PyObject)+++-- | Namespace for the top level code execution. It corresponds to+-- @\__dict\__@ field of a @\__main\__@ module.+--+-- @since 0.2@+data Main = Main++instance Namespace Main where+ basicNamespaceDict _ =+ throwOnNULL =<< Py [CU.block| PyObject* {+ PyObject* main_module = PyImport_AddModule("__main__");+ if( PyErr_Occurred() )+ return NULL;+ PyObject* dict = PyModule_GetDict(main_module);+ Py_XINCREF(dict);+ return dict;+ }|]+++-- | Temporary namespace which get destroyed after execution+--+-- @since 0.2@+data Temp = Temp++instance Namespace Temp where+ basicNamespaceDict _ = basicNewDict+++-- | Newtype wrapper for bare python object. It's assumed to be a+-- dictionary. This is not checked.+--+-- @since 0.2@+newtype DictPtr = DictPtr (Ptr PyObject)++instance Namespace DictPtr where+ basicNamespaceDict (DictPtr p) = p <$ incref p+++-- | Newtype wrapper for python dictionary. It's not checked whether+-- object is actually dictionary.+--+-- @since 0.2@+newtype Dict = Dict PyObject++instance Namespace Dict where+ basicNamespaceDict (Dict d)+ -- NOTE: We're incrementing counter inside bracket so we're safe.+ = unsafeWithPyObject d (basicNamespaceDict . DictPtr)++-- | Newtype wrapper over module object.+--+-- @since 0.2@+newtype ModulePtr = ModulePtr (Ptr PyObject)++instance Namespace ModulePtr where+ basicNamespaceDict (ModulePtr p) = do+ throwOnNULL =<< Py [CU.block| PyObject* {+ PyObject* dict = PyModule_GetDict($(PyObject* p));+ Py_XINCREF(dict);+ return dict;+ }|]++-- | Newtype wrapper over module object.+newtype Module = Module PyObject++instance Namespace Module where+ basicNamespaceDict (Module d)+ -- NOTE: We're incrementing counter inside bracket so we're safe.+ = unsafeWithPyObject d (basicNamespaceDict . ModulePtr)+++-- | Evaluate python expression. This is wrapper over python's @eval@.+--+-- @since 0.2@+eval :: (Namespace global, Namespace local)+ => global -- ^ Data type providing global variables dictionary+ -> local -- ^ Data type providing local variables dictionary+ -> PyQuote -- ^ Source code+ -> Py PyObject+eval globals locals q = runProgram $ do+ p_py <- unsafeWithCode q.code+ p_globals <- takeOwnership =<< progPy (basicNamespaceDict globals)+ p_locals <- takeOwnership =<< progPy (basicNamespaceDict locals)+ progPy $ do+ q.binder.bind p_locals+ p_res <- Py [C.block| PyObject* {+ PyObject* globals = $(PyObject* p_globals);+ PyObject* locals = $(PyObject* p_locals);+ // Compile code+ PyObject *code = Py_CompileString($(char* p_py), "<interactive>", Py_eval_input);+ if( PyErr_Occurred() ) {+ return NULL;+ }+ // Evaluate expression+ PyObject* r = PyEval_EvalCode(code, globals, locals);+ Py_DECREF(code);+ return r;+ }|]+ checkThrowPyError+ newPyObject p_res+{-# SPECIALIZE eval :: Main -> Temp -> PyQuote -> Py PyObject #-}++-- | Evaluate sequence of python statements This is wrapper over python's @exec@.+--+-- @since 0.2@+exec :: (Namespace global, Namespace local)+ => global -- ^ Data type providing global variables dictionary+ -> local -- ^ Data type providing local variables dictionary+ -> PyQuote -- ^ Source code+ -> Py ()+exec globals locals q = runProgram $ do+ p_py <- unsafeWithCode q.code+ p_globals <- takeOwnership =<< progPy (basicNamespaceDict globals)+ p_locals <- takeOwnership =<< progPy (basicNamespaceDict locals)+ progPy $ do+ q.binder.bind p_locals+ Py[C.block| void {+ PyObject* globals = $(PyObject* p_globals);+ PyObject* locals = $(PyObject* p_locals);+ // Compile code+ PyObject *code = Py_CompileString($(char* p_py), "<interactive>", Py_file_input);+ if( PyErr_Occurred() ){+ return;+ }+ // Execute statements+ PyObject* res = PyEval_EvalCode(code, globals, locals);+ Py_XDECREF(res);+ Py_DECREF(code);+ } |]+ checkThrowPyError+{-# SPECIALIZE exec :: Main -> Main -> PyQuote -> Py () #-}+{-# SPECIALIZE exec :: Main -> Temp -> PyQuote -> Py () #-}++-- | Obtain pointer to code+unsafeWithCode :: Code -> Program r (Ptr CChar)+unsafeWithCode (Code bs) = Program $ ContT $ \fun ->+ Py (BS.unsafeUseAsCString bs $ unsafeRunPy . fun) ----------------------------------------------------------------
src/Python/Internal/EvalQQ.hs view
@@ -3,10 +3,7 @@ -- | module Python.Internal.EvalQQ ( -- * Evaluators and QQ- evaluatorPymain- , evaluatorPy_- , evaluatorPye- , evaluatorPyf+ evaluatorPyf -- * Code generation , expQQ , Mode(..)@@ -14,10 +11,12 @@ import Control.Monad.IO.Class import Control.Monad.Catch+import Control.Monad.Trans.Cont (ContT(..)) import Data.Bits import Data.Char import Data.List (intercalate) import Data.ByteString qualified as BS+import Data.ByteString.Unsafe qualified as BS import Data.Text qualified as T import Data.Text.Encoding qualified as T import Foreign.C.Types@@ -42,117 +41,53 @@ C.include "<inline-python.h>" ---------------------------------------------------------------- -------------------------------------------------------------------- Evaluators-----------------------------------------------------------------+-- | Python's variable name encoded using UTF-8. It exists in order to+-- avoid working with @String@ at runtime.+newtype PyVarName = PyVarName BS.ByteString+ deriving stock (Show, TH.Lift) --- | Evaluate expression within context of @__main__@ module. All--- variables defined in this evaluator persist.-pyExecExpr- :: Ptr PyObject -- ^ Globals- -> Ptr PyObject -- ^ Locals- -> String -- ^ Python source code- -> Py ()-pyExecExpr p_globals p_locals src = runProgram $ do- p_py <- withPyCString src- progPy $ do- Py [C.block| void {- PyObject* globals = $(PyObject* p_globals);- PyObject* locals = $(PyObject* p_locals);- // Compile code- PyObject *code = Py_CompileString($(char* p_py), "<interactive>", Py_file_input);- if( PyErr_Occurred() ){- return;- }- // Execute statements- PyObject* res = PyEval_EvalCode(code, globals, locals);- Py_XDECREF(res);- Py_DECREF(code);- } |]- checkThrowPyError+varName :: String -> PyVarName+varName = PyVarName . T.encodeUtf8 . T.pack --- | Evaluate expression with fresh local environment-pyEvalExpr- :: Ptr PyObject -- ^ Globals- -> Ptr PyObject -- ^ Locals- -> String -- ^ Python source code- -> Py PyObject-pyEvalExpr p_globals p_locals src = runProgram $ do- p_py <- withPyCString src- progPy $ do- p_res <- Py [C.block| PyObject* {- PyObject* globals = $(PyObject* p_globals);- PyObject* locals = $(PyObject* p_locals);- // Compile code- PyObject *code = Py_CompileString($(char* p_py), "<interactive>", Py_eval_input);- if( PyErr_Occurred() ) {- return NULL;- }- // Evaluate expression- PyObject* r = PyEval_EvalCode(code, globals, locals);- Py_DECREF(code);- return r;- }|]- checkThrowPyError- newPyObject p_res+unsafeWithPyVarName :: PyVarName -> Program r (Ptr CChar)+unsafeWithPyVarName (PyVarName bs)+ = progIOBracket (BS.unsafeUseAsCString bs) -evaluatorPymain :: (Ptr PyObject -> Py String) -> Py ()-evaluatorPymain getSource = do- p_main <- basicMainDict- src <- getSource p_main- pyExecExpr p_main p_main src+bindVar :: ToPy a => PyVarName -> a -> DictBinder+bindVar var a = DictBinder $ \p_dict -> runProgram $ do+ p_key <- unsafeWithPyVarName var+ p_obj <- takeOwnership =<< progPy (throwOnNULL =<< basicToPy a)+ progPy $ do+ r <- Py [CU.block| int {+ PyObject* p_obj = $(PyObject* p_obj);+ return PyDict_SetItemString($(PyObject* p_dict), $(char* p_key), p_obj);+ } |]+ case r of+ 0 -> pure ()+ _ -> mustThrowPyError -evaluatorPy_ :: (Ptr PyObject -> Py String) -> Py ()-evaluatorPy_ getSource = runProgram $ do- p_globals <- progPy basicMainDict- p_locals <- takeOwnership =<< progPy basicNewDict- progPy $ pyExecExpr p_globals p_locals =<< getSource p_locals -evaluatorPye :: (Ptr PyObject -> Py String) -> Py PyObject-evaluatorPye getSource = runProgram $ do- p_globals <- progPy basicMainDict- p_locals <- takeOwnership =<< progPy basicNewDict- progPy $ pyEvalExpr p_globals p_locals =<< getSource p_locals -evaluatorPyf :: (Ptr PyObject -> Py String) -> Py PyObject-evaluatorPyf getSource = runProgram $ do- p_globals <- progPy basicMainDict- p_locals <- takeOwnership =<< progPy basicNewDict- p_kwargs <- takeOwnership =<< progPy basicNewDict+----------------------------------------------------------------+-- Evaluators+----------------------------------------------------------------++evaluatorPyf :: PyQuote -> Py PyObject+evaluatorPyf (PyQuote code binder) = runProgram $ do+ p_locals <- takeOwnership =<< progPy basicNewDict+ p_kwargs <- takeOwnership =<< progPy basicNewDict progPy $ do -- Create function in p_locals- pyExecExpr p_globals p_locals =<< getSource p_kwargs+ exec Main (DictPtr p_locals) (PyQuote code mempty) -- Look up function p_fun <- getFunctionObject p_locals >>= \case NULL -> throwM $ PyInternalError "_inline_python_ must be present" p -> pure p -- Call python function we just constructed+ binder.bind p_kwargs newPyObject =<< throwOnNULL =<< basicCallKwdOnly p_fun p_kwargs --basicBindInDict :: ToPy a => String -> a -> Ptr PyObject -> Py ()-basicBindInDict name a p_dict = runProgram $ do- p_key <- withPyCString name- p_obj <- takeOwnership =<< progPy (throwOnNULL =<< basicToPy a)- progPy $ do- r <- Py [C.block| int {- PyObject* p_obj = $(PyObject* p_obj);- return PyDict_SetItemString($(PyObject* p_dict), $(char* p_key), p_obj);- } |]- case r of- 0 -> pure ()- _ -> mustThrowPyError---- | Return dict of @__main__@ module-basicMainDict :: Py (Ptr PyObject)-basicMainDict = Py [CU.block| PyObject* {- PyObject* main_module = PyImport_AddModule("__main__");- if( PyErr_Occurred() )- return NULL;- return PyModule_GetDict(main_module);- }|]- getFunctionObject :: Ptr PyObject -> Py (Ptr PyObject) getFunctionObject p_dict = do Py [CU.exp| PyObject* { PyDict_GetItemString($(PyObject *p_dict), "_inline_python_") } |]@@ -205,14 +140,13 @@ case code of ExitSuccess -> pure $ words stdout ExitFailure{} -> fail stderr- let args = [ [| basicBindInDict $(TH.lift nm) $(TH.dyn (chop nm)) |]+ let args = [ [| bindVar $(TH.lift (varName nm)) $(TH.dyn (chop nm)) |] | nm <- antis ] src_eval = prepareForEval mode antis src --- [| \p_dict -> do- mapM_ ($ p_dict) $(TH.listE args)- pure $(TH.lift src_eval)+ [| PyQuote ($(TH.lift $ codeFromString src_eval))+ (mconcat $(TH.listE args)) |]
src/Python/Internal/Program.hs view
@@ -6,6 +6,7 @@ , runProgram , progPy , progIO+ , progIOBracket -- * Control flow , abort , abortM@@ -63,6 +64,9 @@ progPy :: Py a -> Program r a progPy = Program . lift++progIOBracket :: ((a -> IO r) -> IO r) -> Program r a+progIOBracket = coerce -- | Early exit from continuation monad. abort :: r -> Program r a
src/Python/Internal/Types.hs view
@@ -9,6 +9,7 @@ module Python.Internal.Types ( -- * Data type PyObject(..)+ , withPyObject , unsafeWithPyObject , PyThreadState , PyError(..)@@ -16,6 +17,12 @@ , PyInternalError(..) , Py(..) , pyIO+ -- ** Python code wrappers+ , PyQuote(..)+ , Code(..)+ , codeFromText+ , codeFromString+ , DictBinder(..) -- * inline-C , pyCtx -- * Patterns@@ -23,6 +30,8 @@ , pattern IPY_ERR_COMPILE , pattern IPY_ERR_PYTHON , pattern NULL+ , pattern FALSE+ , pattern TRUE ) where import Control.Monad.IO.Class@@ -31,9 +40,13 @@ import Control.Exception import Data.Coerce import Data.Int+import Data.ByteString qualified as BS import Data.Map.Strict qualified as Map+import Data.Text qualified as T+import Data.Text.Encoding qualified as T import Foreign.Ptr import Foreign.C.Types+import Language.Haskell.TH.Syntax qualified as TH import GHC.ForeignPtr import Language.C.Types@@ -52,6 +65,9 @@ newtype PyObject = PyObject (ForeignPtr PyObject) deriving stock Show +withPyObject :: forall a. PyObject -> (Ptr PyObject -> Py a) -> Py a+withPyObject = coerce (withForeignPtr @PyObject @a)+ unsafeWithPyObject :: forall a. PyObject -> (Ptr PyObject -> Py a) -> Py a unsafeWithPyObject = coerce (unsafeWithForeignPtr @PyObject @a) @@ -71,7 +87,7 @@ | PythonNotInitialized -- ^ Python interpreter is not initialized | PythonIsFinalized- -- ^ Python interpreter is not initialized + -- ^ Python interpreter is not initialized deriving stock (Show) deriving anyclass (Exception) @@ -115,6 +131,53 @@ ----------------------------------------------------------------+-- Code wrappers+----------------------------------------------------------------++-- | Quasiquoted python code. It contains source code and closure+-- which populates dictionary with local variables. @PyQuote@ value+-- which captures local variables could be created using+-- 'Python.Inline.QQ.pycode' quasiquoter.+--+-- @since 0.2@+data PyQuote = PyQuote+ { code :: !Code+ , binder :: !DictBinder+ }+++-- | UTF-8 encoded python source code.+--+-- @since 0.2@+newtype Code = Code BS.ByteString+ deriving stock (Show, TH.Lift)++-- | Create properly encoded @Code@. This function doesn't check+-- syntactic validity.+--+-- @since 0.2@+codeFromText :: T.Text -> Code+codeFromText = Code . T.encodeUtf8++-- | Create properly encoded @Code@. This function doesn't check+-- syntactic validity.+--+-- @since 0.2@+codeFromString :: String -> Code+codeFromString = codeFromText . T.pack++-- | Closure which stores values in provided python dictionary.+--+-- @since 0.2@+newtype DictBinder = DictBinder { bind :: Ptr PyObject -> Py () }++instance Semigroup DictBinder where+ f <> g = DictBinder $ \p -> f.bind p >> g.bind p+instance Monoid DictBinder where+ mempty = DictBinder $ \_ -> pure ()+++---------------------------------------------------------------- -- inline-C ---------------------------------------------------------------- @@ -148,3 +211,9 @@ pattern NULL :: Ptr a pattern NULL <- ((== nullPtr) -> True) where NULL = nullPtr++pattern FALSE :: CInt+pattern FALSE = 0++pattern TRUE :: CInt+pattern TRUE <- ((/= 0) -> True)
test/TST/FromPy.hs view
@@ -1,13 +1,19 @@ {-# LANGUAGE AllowAmbiguousTypes #-}+{-# LANGUAGE OverloadedStrings #-} -- | module TST.FromPy (tests) where +import Data.ByteString qualified as BS import Control.Monad.IO.Class import Test.Tasty import Test.Tasty.HUnit import Python.Inline import Python.Inline.QQ+import Data.Complex (Complex((:+))) +import TST.Util++ tests :: TestTree tests = testGroup "FromPy" [ testGroup "Int"@@ -20,6 +26,13 @@ , testCase "Double->Double" $ eq @Double (Just 1234.25) [pye| 1234.25 |] , testCase "None->Double" $ eq @Double Nothing [pye| None |] ]+ , testGroup "Complex"+ [ testCase "Int->Complex" $ eq @(Complex Double) (Just 1234) [pye| 1234 |]+ , testCase "Double->Complex" $ eq @(Complex Double) (Just 1234.25) [pye| 1234.25 |]+ , testCase "Complex->Complex" $ eq @(Complex Double) (Just $ 1234.5 :+ 6789)+ [pye| 1234.5+6789.0j |]+ , testCase "None->Complex" $ eq @(Complex Double) Nothing [pye| None |]+ ] , testGroup "Char" [ testCase "0" $ eq @Char Nothing [pye| "" |] , testCase "1 1B" $ eq @Char (Just 'a') [pye| "a" |]@@ -31,6 +44,12 @@ [ testCase "asdf" $ eq @String (Just "asdf") [pye| "asdf" |] , testCase "фыва" $ eq @String (Just "фыва") [pye| "фыва" |] ]+ , testGroup "ByteString"+ [ testCase "empty" $ eq @BS.ByteString (Just "") [pye| b'' |]+ , testCase "x00" $ eq @BS.ByteString (Just $ BS.pack [0]) [pye| b'\x00' |]+ , testCase "empty arr" $ eq @BS.ByteString (Just "") [pye| bytearray(b'') |]+ , testCase "x00 arr" $ eq @BS.ByteString (Just $ BS.pack [0]) [pye| bytearray(b'\x00') |]+ ] , testGroup "Bool" [ testCase "True->Bool" $ eq @Bool (Just True) [pye| True |] , testCase "False->Bool" $ eq @Bool (Just False) [pye| False |]@@ -74,9 +93,6 @@ , testCase "Int" $ eq @[Int] Nothing [pye| None |] ] ]--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 -> Py () failE p = fromPyEither @a p >>= \case
test/TST/Roundtrip.hs view
@@ -8,14 +8,22 @@ import Data.Typeable import Data.Set (Set) import Data.Map.Strict (Map)+import Data.Text qualified as T+import Data.Text.Lazy qualified as TL+import Data.Complex (Complex) import Foreign.C.Types import Test.Tasty import Test.Tasty.QuickCheck import Test.QuickCheck.Instances.Vector ()+import Test.QuickCheck.Instances.ByteString ()+import Test.QuickCheck.Instances.Text () import Python.Inline import Python.Inline.QQ +import Data.ByteString qualified as BS+import Data.ByteString.Lazy qualified as BL+import Data.ByteString.Short qualified as SBS import Data.Vector qualified as V #if MIN_VERSION_vector(0,13,2) import Data.Vector.Strict qualified as VV@@ -54,6 +62,9 @@ -- Floating point , testRoundtrip @Double , testRoundtrip @Float+ -- Complex+ , testRoundtrip @(Complex Double)+ , testRoundtrip @(Complex Float) -- Other scalars , testRoundtrip @Char , testRoundtrip @Bool@@ -62,8 +73,11 @@ , testRoundtrip @(Int,(Int,Int)) , testRoundtrip @(Int,Int,Int) , testRoundtrip @(Int,Int,Int,Char)+ , testRoundtrip @(Maybe Int)+ , testRoundtrip @(Maybe T.Text) , testRoundtrip @[Int] , testRoundtrip @[[Int]]+ , testRoundtrip @[Complex Double] , testRoundtrip @(Set Int) , testRoundtrip @(Map Int Int) -- , testRoundtrip @String -- Trips on zero byte as it should@@ -74,6 +88,11 @@ #if MIN_VERSION_vector(0,13,2) -- , testRoundtrip @(VV.Vector Int) #endif+ , testRoundtrip @BS.ByteString+ , testRoundtrip @BL.ByteString+ , testRoundtrip @SBS.ShortByteString+ , testRoundtrip @T.Text+ , testRoundtrip @TL.Text ] , testGroup "OutOfRange" [ testOutOfRange @Int8 @Int16
test/TST/Run.hs view
@@ -6,10 +6,12 @@ import Control.Exception import Control.Monad import Control.Monad.IO.Class+import Data.Map.Strict qualified as Map import Test.Tasty import Test.Tasty.HUnit import Python.Inline import Python.Inline.QQ+import Python.Inline.Eval import TST.Util tests :: TestTree@@ -130,6 +132,37 @@ assert False, "x shouln't be visible (2)" except NameError: pass+ |]+ , testCase "pyf works" $ do+ let x = 12 :: Int+ eq (Just (482412::Int)) [pyf|+ xs = [i*x_hs for i in [1, 200, 40000]]+ return sum(xs)+ |]+ , testCase "exec with Dict" $ runPy $ do+ dct <- [pye| {} |]+ exec Main (Dict dct) [pycode|+ a = 12+ b = 13+ |]+ throwsPy $ exec Main (Module dct) [pycode| |]+ d <- fromPy dct+ liftIO $ assertEqual "dict" (Just (Map.fromList [("a",12::Int),("b",13)])) d+ , testCase "exec with Module" $ runPy $ do+ m <- [pyf|+ import importlib.util+ spec = importlib.util.spec_from_loader("dyn", loader=None)+ return importlib.util.module_from_spec(spec)+ |]+ exec Main (Module m) [pycode|+ a = 12+ b = 'asd'+ |]+ [py_|+ import types+ isinstance(m_hs, types.ModuleType)+ assert m_hs.a == 12+ assert m_hs.b == 'asd' |] ]
test/TST/ToPy.hs view
@@ -1,8 +1,11 @@+{-# LANGUAGE OverloadedStrings #-} -- | module TST.ToPy (tests) where -import Data.Set qualified as Set-import Data.Map.Strict qualified as Map+import Data.ByteString qualified as BS+import Data.Set qualified as Set+import Data.Map.Strict qualified as Map+import Data.Complex (Complex((:+))) import Test.Tasty import Test.Tasty.HUnit import Python.Inline@@ -14,10 +17,18 @@ tests = testGroup "ToPy" [ 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 "Complex" $ runPy $+ let z = 5.5 :+ 7.5 :: Complex Double+ in [py_| assert (z_hs.real == 5.5); assert (z_hs.imag == 7.5)|] , 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 == 'фыва' |]+ , testCase "String ASCII" $ runPy $ let c = "asdf"::String in [py_| assert c_hs == 'asdf' |]+ , testCase "String unicode" $ runPy $ let c = "фыва"::String in [py_| assert c_hs == 'фыва' |]+ -- Byte objects+ , testCase "empty ByteString" $ runPy $+ let bs = BS.empty in [py_| assert bs_hs == b'' |]+ , testCase "0 ByteString" $ runPy $+ let bs = BS.pack [0] in [py_| assert bs_hs == b'\x00' |] -- Container types , testCase "Tuple2" $ runPy $ let x = (1::Int, 333::Int)
test/TST/Util.hs view
@@ -6,7 +6,6 @@ import Test.Tasty.HUnit import Python.Inline-import Python.Inline.Types throwsPy :: Py () -> Py () throwsPy io = (io >> liftIO (assertFailure "Evaluation should raise python exception"))@@ -16,3 +15,5 @@ throwsPyIO io = (io >> assertFailure "Evaluation should raise python exception") `catch` (\(_::PyError) -> pure ()) +eq :: (Eq a, Show a, FromPy a) => Maybe a -> (Py PyObject) -> IO ()+eq a action = assertEqual "fromPy: " a =<< runPy (fromPy =<< action)