Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions ChangeLog.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
NEXT_VERSION [..]
0.2 [2025.05.04]
----------------
* `FromPy`/`ToPy` instances for text and bytestrings data types.
* `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]
--------------------
Expand Down
2 changes: 1 addition & 1 deletion inline-python.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ Cabal-Version: 3.0
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
Expand Down
46 changes: 40 additions & 6 deletions src/Python/Inline/Eval.hs
Original file line number Diff line number Diff line change
@@ -1,25 +1,59 @@
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TemplateHaskell #-}
-- |
-- Interface to python's @eval@ and @exec@
-- 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(..)
-- ** Data types
, Code
, codeFromText
, codeFromString
, DictBinder
) 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

54 changes: 44 additions & 10 deletions src/Python/Inline/Literal.hs
Original file line number Diff line number Diff line change
Expand Up @@ -208,17 +208,21 @@ deriving via CDouble instance FromPy Double
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)) } |]
Expand Down Expand Up @@ -412,6 +416,24 @@ instance (FromPy a, FromPy b, FromPy c, FromPy d) => FromPy (a,b,c,d) where
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

Expand Down Expand Up @@ -566,7 +588,9 @@ vectorToPy vec = runProgram $ do
n_c = fromIntegral n :: CLLong


-- | @since NEXT_VERSION@. Converted to @bytes@
-- | 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
Expand All @@ -575,7 +599,9 @@ instance ToPy BS.ByteString where
NULL -> unsafeRunPy mustThrowPyError
_ -> return py

-- | @since NEXT_VERSION@. Accepts @bytes@ and @bytearray@
-- | Accepts @bytes@ and @bytearray@
--
-- @since 0.2
instance FromPy BS.ByteString where
basicFromPy py = pyIO $ do
[CU.exp| int { PyBytes_Check($(PyObject* py)) } |] >>= \case
Expand All @@ -595,16 +621,22 @@ instance FromPy BS.ByteString where
copyBytes hs_buf py_buf sz
BS.unsafePackMallocCStringLen (hs_buf, sz)

-- | @since NEXT_VERSION@. Converted to @bytes@
-- | Converted to @bytes@
--
-- @since 0.2
instance ToPy BL.ByteString where
basicToPy = basicToPy . BL.toStrict

-- | @since NEXT_VERSION@. Accepts @bytes@ and @bytearray@
-- | Accepts @bytes@ and @bytearray@
--
-- @since 0.2
instance FromPy BL.ByteString where
basicFromPy = fmap BL.fromStrict . basicFromPy


-- | @since NEXT_VERSION@. Accepts @bytes@ and @bytearray@
-- | Accepts @bytes@ and @bytearray@
--
-- @since 0.2
instance FromPy SBS.ShortByteString where
basicFromPy py = pyIO $ do
[CU.exp| int { PyBytes_Check($(PyObject* py)) } |] >>= \case
Expand All @@ -623,7 +655,9 @@ instance FromPy SBS.ShortByteString where
bs <- BS.unsafePackCStringLen (buf, sz)
evaluate $ SBS.toShort bs

-- | @since NEXT_VERSION@. Converted to @bytes@
-- | 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
Expand All @@ -633,7 +667,7 @@ instance ToPy SBS.ShortByteString where
_ -> return py


-- | @since NEXT_VERSION@.
-- | @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
Expand All @@ -645,11 +679,11 @@ instance ToPy T.Text where
where
bs = T.encodeUtf8 str

-- | @since NEXT_VERSION@.
-- | @since 0.2@.
instance ToPy TL.Text where
basicToPy = basicToPy . TL.toStrict

-- | @since NEXT_VERSION@.
-- | @since 0.2@.
instance FromPy T.Text where
basicFromPy py = pyIO $ do
[CU.exp| int { PyUnicode_Check($(PyObject* py)) } |] >>= \case
Expand All @@ -660,7 +694,7 @@ instance FromPy T.Text where
return $! T.decodeUtf8Lenient bs
_ -> throwM BadPyType

-- | @since NEXT_VERSION@.
-- | @since 0.2@.
instance FromPy TL.Text where
basicFromPy = fmap TL.fromStrict . basicFromPy

Expand Down
3 changes: 3 additions & 0 deletions src/Python/Inline/QQ.hs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@
-- > 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_
Expand Down
15 changes: 8 additions & 7 deletions src/Python/Internal/Eval.hs
Original file line number Diff line number Diff line change
Expand Up @@ -683,12 +683,13 @@ checkThrowBadPyType = do
--
-- @since 0.2@
class Namespace a where
-- | Returns dictionary object. Caller takes ownership of returned
-- object.
-- | Returns dictionary object. Caller should take ownership of
-- returned object.
basicNamespaceDict :: a -> Py (Ptr PyObject)


-- | Namespace for the top level code execution.
-- | Namespace for the top level code execution. It corresponds to
-- @\__dict\__@ field of a @\__main\__@ module.
--
-- @since 0.2@
data Main = Main
Expand Down Expand Up @@ -724,8 +725,8 @@ instance Namespace DictPtr where
basicNamespaceDict (DictPtr p) = p <$ incref p


-- | Newtype wrapper for bare python object. It's assumed to be a
-- dictionary. This is not checked.
-- | Newtype wrapper for python dictionary. It's not checked whether
-- object is actually dictionary.
--
-- @since 0.2@
newtype Dict = Dict PyObject
Expand Down Expand Up @@ -757,7 +758,7 @@ instance Namespace Module where
= unsafeWithPyObject d (basicNamespaceDict . ModulePtr)


-- | Evaluate python expression
-- | Evaluate python expression. This is wrapper over python's @eval@.
--
-- @since 0.2@
eval :: (Namespace global, Namespace local)
Expand Down Expand Up @@ -788,7 +789,7 @@ eval globals locals q = runProgram $ do
newPyObject p_res
{-# SPECIALIZE eval :: Main -> Temp -> PyQuote -> Py PyObject #-}

-- | Evaluate sequence of python statements
-- | Evaluate sequence of python statements This is wrapper over python's @exec@.
--
-- @since 0.2@
exec :: (Namespace global, Namespace local)
Expand Down
4 changes: 2 additions & 2 deletions src/Python/Internal/EvalQQ.hs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

import Control.Monad.IO.Class
import Control.Monad.Catch
import Control.Monad.Trans.Cont (ContT(..))

Check warning on line 14 in src/Python/Internal/EvalQQ.hs

View workflow job for this annotation

GitHub Actions / ubuntu-latest / ghc 9.4.8

The import of ‘Control.Monad.Trans.Cont’ is redundant

Check warning on line 14 in src/Python/Internal/EvalQQ.hs

View workflow job for this annotation

GitHub Actions / ubuntu-latest / ghc 9.2.8

The import of ‘Control.Monad.Trans.Cont’ is redundant

Check warning on line 14 in src/Python/Internal/EvalQQ.hs

View workflow job for this annotation

GitHub Actions / ubuntu-latest / ghc 9.6.6

The import of ‘Control.Monad.Trans.Cont’ is redundant

Check warning on line 14 in src/Python/Internal/EvalQQ.hs

View workflow job for this annotation

GitHub Actions / ubuntu-latest / ghc 9.8.2

The import of ‘Control.Monad.Trans.Cont’ is redundant

Check warning on line 14 in src/Python/Internal/EvalQQ.hs

View workflow job for this annotation

GitHub Actions / ubuntu-latest / ghc 9.10.1

The import of ‘Control.Monad.Trans.Cont’ is redundant
import Data.Bits
import Data.Char
import Data.List (intercalate)
Expand Down Expand Up @@ -50,8 +50,8 @@
varName = PyVarName . T.encodeUtf8 . T.pack

unsafeWithPyVarName :: PyVarName -> Program r (Ptr CChar)
unsafeWithPyVarName (PyVarName bs) = Program $ ContT $ \fun ->
Py (BS.unsafeUseAsCString bs $ unsafeRunPy . fun)
unsafeWithPyVarName (PyVarName bs)
= progIOBracket (BS.unsafeUseAsCString bs)


bindVar :: ToPy a => PyVarName -> a -> DictBinder
Expand Down
4 changes: 4 additions & 0 deletions src/Python/Internal/Program.hs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ module Python.Internal.Program
, runProgram
, progPy
, progIO
, progIOBracket
-- * Control flow
, abort
, abortM
Expand Down Expand Up @@ -64,6 +65,9 @@ progIO = Program . lift . pyIO
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
abort r = Program $ ContT $ \_ -> pure r
Expand Down
9 changes: 5 additions & 4 deletions src/Python/Internal/Types.hs
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,9 @@ instance PrimMonad Py where
----------------------------------------------------------------

-- | Quasiquoted python code. It contains source code and closure
-- which populates dictionary with local variables.
-- 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
Expand All @@ -144,8 +146,7 @@ data PyQuote = PyQuote
}


-- | UTF-8 encoded python source code. Usually it's produced by
-- Template Haskell's 'TH.lift' function.
-- | UTF-8 encoded python source code.
--
-- @since 0.2@
newtype Code = Code BS.ByteString
Expand All @@ -165,7 +166,7 @@ codeFromText = Code . T.encodeUtf8
codeFromString :: String -> Code
codeFromString = codeFromText . T.pack

-- | Closure which stores values in provided dictionary
-- | Closure which stores values in provided python dictionary.
--
-- @since 0.2@
newtype DictBinder = DictBinder { bind :: Ptr PyObject -> Py () }
Expand Down
2 changes: 2 additions & 0 deletions test/TST/Roundtrip.hs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
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

Check warning on line 29 in test/TST/Roundtrip.hs

View workflow job for this annotation

GitHub Actions / ubuntu-latest / ghc 9.4.8

The qualified import of ‘Data.Vector.Strict’ is redundant

Check warning on line 29 in test/TST/Roundtrip.hs

View workflow job for this annotation

GitHub Actions / ubuntu-latest / ghc 9.2.8

The qualified import of ‘Data.Vector.Strict’ is redundant

Check warning on line 29 in test/TST/Roundtrip.hs

View workflow job for this annotation

GitHub Actions / ubuntu-latest / ghc 9.6.6

The qualified import of ‘Data.Vector.Strict’ is redundant

Check warning on line 29 in test/TST/Roundtrip.hs

View workflow job for this annotation

GitHub Actions / ubuntu-latest / ghc 9.8.2

The qualified import of ‘Data.Vector.Strict’ is redundant

Check warning on line 29 in test/TST/Roundtrip.hs

View workflow job for this annotation

GitHub Actions / ubuntu-latest / ghc 9.10.1

The qualified import of ‘Data.Vector.Strict’ is redundant
#endif
import Data.Vector.Storable qualified as VS
import Data.Vector.Primitive qualified as VP
Expand Down Expand Up @@ -73,6 +73,8 @@
, 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]
Expand Down