cpython 3.5.0 → 3.5.1
raw patch · 5 files changed
+401/−3 lines, 5 filesPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
+ CPython.Simple: Arg :: a -> Arg
+ CPython.Simple: arg :: ToPy a => a -> Arg
+ CPython.Simple: call :: FromPy a => Text -> Text -> [Arg] -> [(Text, Arg)] -> IO a
+ CPython.Simple: data Arg
+ CPython.Simple: getAttribute :: FromPy a => Text -> Text -> IO a
+ CPython.Simple: importModule :: Text -> IO Module
+ CPython.Simple: initialize :: IO ()
+ CPython.Simple: instance CPython.Simple.Instances.ToPy CPython.Simple.Arg
+ CPython.Simple: setAttribute :: ToPy a => Text -> Text -> a -> IO ()
+ CPython.Simple.Instances: PyCastException :: String -> PyCastException
+ CPython.Simple.Instances: class FromPy a
+ CPython.Simple.Instances: class ToPy a
+ CPython.Simple.Instances: data PyCastException
+ CPython.Simple.Instances: easyFromPy :: (Concrete b, Typeable c) => (b -> IO c) -> Proxy c -> SomeObject -> IO c
+ CPython.Simple.Instances: easyToPy :: Object c => (a -> IO c) -> a -> IO SomeObject
+ CPython.Simple.Instances: fromPy :: FromPy a => SomeObject -> IO a
+ CPython.Simple.Instances: instance (CPython.Simple.Instances.FromPy a, CPython.Simple.Instances.FromPy b) => CPython.Simple.Instances.FromPy (a, b)
+ CPython.Simple.Instances: instance (CPython.Simple.Instances.FromPy a, CPython.Simple.Instances.FromPy b, CPython.Simple.Instances.FromPy c) => CPython.Simple.Instances.FromPy (a, b, c)
+ CPython.Simple.Instances: instance CPython.Simple.Instances.FromPy ()
+ CPython.Simple.Instances: instance CPython.Simple.Instances.FromPy Data.Text.Internal.Text
+ CPython.Simple.Instances: instance CPython.Simple.Instances.FromPy GHC.Base.String
+ CPython.Simple.Instances: instance CPython.Simple.Instances.FromPy GHC.Integer.Type.Integer
+ CPython.Simple.Instances: instance CPython.Simple.Instances.FromPy GHC.Types.Char
+ CPython.Simple.Instances: instance CPython.Simple.Instances.FromPy GHC.Types.Double
+ CPython.Simple.Instances: instance CPython.Simple.Instances.FromPy a => CPython.Simple.Instances.FromPy (GHC.Maybe.Maybe a)
+ CPython.Simple.Instances: instance CPython.Simple.Instances.FromPy a => CPython.Simple.Instances.FromPy [a]
+ CPython.Simple.Instances: instance CPython.Simple.Instances.ToPy Data.Text.Internal.Text
+ CPython.Simple.Instances: instance CPython.Simple.Instances.ToPy GHC.Base.String
+ CPython.Simple.Instances: instance CPython.Simple.Instances.ToPy GHC.Integer.Type.Integer
+ CPython.Simple.Instances: instance CPython.Simple.Instances.ToPy GHC.Types.Char
+ CPython.Simple.Instances: instance CPython.Simple.Instances.ToPy GHC.Types.Double
+ CPython.Simple.Instances: instance CPython.Simple.Instances.ToPy a => CPython.Simple.Instances.ToPy (GHC.Maybe.Maybe a)
+ CPython.Simple.Instances: instance CPython.Simple.Instances.ToPy a => CPython.Simple.Instances.ToPy [a]
+ CPython.Simple.Instances: instance GHC.Exception.Type.Exception CPython.Simple.Instances.PyCastException
+ CPython.Simple.Instances: instance GHC.Show.Show CPython.Simple.Instances.PyCastException
+ CPython.Simple.Instances: toPy :: ToPy a => a -> IO SomeObject
Files
- README.md +188/−0
- changelog.md +5/−0
- cpython.cabal +7/−3
- lib/CPython/Simple.hs +82/−0
- lib/CPython/Simple/Instances.hs +119/−0
+ README.md view
@@ -0,0 +1,188 @@+# Haskell CPython C bindings [](https://travis-ci.org/zsedem/haskell-cpython)++This library provides C bindings to more or less all of the python3 C API.++> WARNING: Note that the python 3 C API might be quite stable, BUT the [ABI](https://docs.python.org/3/c-api/stable.html) is not, which+means if you compiled with a certain minor version (3.7.1) you should run your program+with the same minor version (3.7.x). (Using docker or nix to package your program is enough+to avoid this problem)++## Writing a Haskell wrapper over a Python module++The easiest way to get started is to `import CPython.Simple`++The `Simple` API surface is fairly small, so if you're doing something fancy you may need to dip into other parts of `CPython`++### General Info++`initialize :: IO ()` kicks off talking to Python, and will need to be called before using other functions.++The `ToPy` and `FromPy` instances are what let us convert Haskell values to and from the corresponding Python values. There are `easyToPy` and `easyFromPy` helpers to help you easily write your own instances in common cases. If you find an instance for some common Haskell type is missing, please submit a PR!++`Arg` is a type representing an argument to a Python function, which lets us use various Haskell types in the same list of arguments.++```haskell+sampleArgs :: [Arg]+sampleArgs =+ [ arg (7 :: Integer)+ , arg ("hello" :: Text)+ ]+```++### Calling Functions++The most common use case is to call some Python function, which we can do with `call`++```haskell+call+ :: FromPy a+ => Text -- ^ module name+ -> Text -- ^ function name+ -> [Arg] -- ^ args+ -> [(Text, Arg)] -- ^ keyword args+ -> IO a+```++For example, if we wanted to wrap Python's `random.randint(low, high)`, we could write this:++```haskell+randint :: Integer -> Integer -> IO Integer+randint low high =+ call "random" "randint" [arg low, arg high] []+```++Because of the `FromPy` instance in `call`'s type signature, we can infer what to do to convert a Python value back into Haskell, if given the type++```haskell+uniform :: Integer -> Integer -> IO Double+uniform low high =+ call "random" "uniform" [arg low, arg high] []+```++We can also use the `TypeApplications` language extension to do this, if needed++```haskell+call @Double "random" "uniform" [arg low, arg high] []+```++Calling a function with mixed positional and keyword arguments is also fairly straightforward:++```haskell+moveToDuration :: Integer -> Integer -> Double -> IO ()+moveToDuration x y seconds =+ call "pyautogui" "moveTo" [arg x, arg y] [("duration", arg seconds)]+```++### Getting and Setting Attributes++`getAttribute` lets us get the value of an attribute of some Python module++```haskell+getAttribute+ :: FromPy a+ => Text -- ^ module name+ -> Text -- ^ attribute name+ -> IO a+```++Here, we get the value of `random.BPF`++```haskell+getBpf :: IO Integer+getBpf = getAttribute "random" "BPF"+```++Likewise, `setAttribute` lets us set the value of an attribute++```haskell+setAttribute+ :: ToPy a+ => Text -- ^ module name+ -> Text -- ^ attribute name+ -> a -- ^ value to set attribute to+ -> IO ()+```++Here's how we can set `random.BPF` to some given number `n`++```haskell+setBpf :: Integer -> IO ()+setBpf n = setAttribute "random" "BPF" n+```++## Using the Low Level API++Sometimes it might be useful to use the less simpler API, especially if you are+already familiar with the [CPython C API](https://docs.python.org/3/c-api/index.html).+This API comes with one-on-one connections between the C API methods and the Haskell methods,+but you won't have to write FFI code directly (like calling incref/decref for Python GC).++After you are familiar with the concepts from the C API, you can search for+methods in the [API docs](http://hackage.haskell.org/package/cpython-3.5.0) on hackage++These examples below should help you start with using the API, by showing the +equivalent haskell code to implement the same as the python example.++### Using `builtins.sum` function+```haskell+sumWithPy :: [Integer] -> IO Int+sumWithPy intlist = do+ testList <- traverse toObj intlist >>= PyList.toList >>= (return . Py.toObject)+ builtinsModule <- Py.importModule "builtins"+ sumFunc <- PyUnicode.toUnicode "sum" >>= Py.getAttribute builtinsModule+ args <- PyTuple.toTuple [testList]+ kwargs <- PyDict.new+ Py.call sumFunc args kwargs >>= castToNumber >>= Py.toInteger >>= PyInt.fromInteger+ where+ castToNumber obj = do x <- Py.castToNumber obj+ return $ fromMaybe (error "not a number returned from the sum") x+ toObj integer = fmap Py.toObject $ PyInt.toInteger integer+```+This example should show you how different it is to call python from strongly typed code, because you have to+handle every bit of the errors, like getting an attribute of a module or just creating new python objects.++```python+intlist = [1, 10, 100, 42]+sum(intlist)+```+### Printing traceback from python+This example is an approach to handle python exceptions, like python would do it, so if an exception comes, we print a traceback+```haskell+{-# LANGUAGE OverloadedStrings #-}+module Main+ ( main ) where++import qualified CPython as Py+import qualified CPython.Types.Module as Py+import qualified CPython.Types.Dictionary as PyDict+import qualified CPython.Types.List as PyList+import qualified CPython.Types.Tuple as PyTuple+import qualified CPython.Types.Unicode as PyUnicode+import qualified CPython.Types.Exception as PyExc+import Data.Text()+import Control.Exception(handle)++main :: IO ()+main = handle pyExceptionHandler $ do+ Py.initialize+ callingSomePython+ Py.finalize+ where+ pyExceptionHandler :: PyExc.Exception -> IO ()+ pyExceptionHandler exception = handle pyExceptionHandlerWithoutPythonTraceback $ do+ tracebackModule <- Py.importModule "traceback"+ print_exc <- PyUnicode.toUnicode "print_exception" >>= Py.getAttribute tracebackModule+ kwargs <- PyDict.new+ args <- case PyExc.exceptionTraceback exception of+ Just tb -> PyTuple.toTuple [PyExc.exceptionType exception, PyExc.exceptionValue exception, tb]+ _ -> PyTuple.toTuple [PyExc.exceptionType exception, PyExc.exceptionValue exception]+ _ <- Py.call print_exc args kwargs+ return ()+ pyExceptionHandlerWithoutPythonTraceback :: PyExc.Exception -> IO ()+ pyExceptionHandlerWithoutPythonTraceback exception = do+ print exception+ putStrLn "Unexpected Python exception (Please report a bug)"++callingSomePython :: IO ()+callingSomePython = do ...+```
changelog.md view
@@ -1,3 +1,8 @@+# 3.5.1++* add new API for simple usage (See readme for details)+* improve hackage page+ # 3.5.0 * made the compilation compatible with different python3 versions
cpython.cabal view
@@ -1,14 +1,16 @@ name: cpython-version: 3.5.0+version: 3.5.1 license: GPL-3 license-file: license.txt author: John Millikin <jmillikin@gmail.com> maintainer: Adam Zsigmond <zsedem@gmail.com> build-type: Simple-cabal-version: >= 1.10+cabal-version: 1.18 category: Foreign homepage: https://github.com/zsedem/haskell-cpython-extra-source-files: changelog.md+extra-doc-files:+ changelog.md+ README.md synopsis: Bindings for libpython description: These bindings allow Haskell code to call CPython code. It is not@@ -77,6 +79,8 @@ CPython.Protocols.Sequence CPython.Constants CPython.Reflection+ CPython.Simple+ CPython.Simple.Instances CPython.System other-modules:
+ lib/CPython/Simple.hs view
@@ -0,0 +1,82 @@+{-# LANGUAGE ExistentialQuantification #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}++module CPython.Simple where++import CPython.Simple.Instances++import Control.Exception (catch, SomeException)+import Data.Semigroup ((<>))+import Data.Text (Text)+import qualified Data.Text as T++import qualified CPython as Py+import qualified CPython.Protocols.Object as Py+import qualified CPython.Types as Py+import qualified CPython.Types.Module as Py+import qualified CPython.Types.Tuple as Py (toTuple)+import qualified CPython.Types.Dictionary as PyDict++data Arg = forall a. ToPy a => Arg a++instance ToPy Arg where+ toPy (Arg a) = toPy a++arg :: ToPy a => a -> Arg+arg = Arg++initialize :: IO ()+initialize = Py.initialize++importModule :: Text -> IO Py.Module+importModule module_ = Py.importModule module_++call+ :: FromPy a+ => Text -- ^ module name+ -> Text -- ^ function name+ -> [Arg] -- ^ args+ -> [(Text, Arg)] -- ^ kwargs+ -> IO a+call moduleName func args kwargs = do+ module_ <- importModule moduleName+ pyFunc <- Py.getAttribute module_ =<< Py.toUnicode func+ pyArgs <- mapM toPy args+ pyArgsTuple <- Py.toTuple pyArgs+ pyKwargs <- toPyKwargs kwargs+ result <- Py.call pyFunc pyArgsTuple pyKwargs+ fromPy result+ where+ toPyKwargs :: [(Text, Arg)] -> IO Py.Dictionary+ toPyKwargs dict = do+ myDict <- PyDict.new+ mapM_+ (\(k, v) -> do+ pyKey <- toPy k+ pyVal <- toPy v+ PyDict.setItem myDict pyKey pyVal)+ dict+ pure myDict++setAttribute+ :: ToPy a+ => Text -- ^ module name+ -> Text -- ^ attribute name+ -> a -- ^ value to set attribute to+ -> IO ()+setAttribute moduleName name value = do+ module_ <- importModule moduleName+ pyName <- Py.toUnicode name+ pyValue <- toPy value+ Py.setAttribute module_ pyName pyValue++getAttribute+ :: FromPy a+ => Text -- ^ module name+ -> Text -- ^ attribute name+ -> IO a+getAttribute moduleName name = do+ module_ <- importModule moduleName+ attr <- Py.getAttribute module_ =<< Py.toUnicode name+ fromPy attr
+ lib/CPython/Simple/Instances.hs view
@@ -0,0 +1,119 @@+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedStrings #-}++module CPython.Simple.Instances where++import Control.Exception (Exception(..), throwIO)+import Control.Monad ((<=<))+import Data.Text (Text)+import qualified Data.Text as T+import Data.Typeable++import qualified CPython.Constants as Py+import qualified CPython.Protocols.Object as Py+import qualified CPython.Types as Py+import qualified CPython.Types.Tuple as Py (fromTuple)++-- TODO: ToPy/FromPy for Bool will require some library changes (e.g. adding fromBool)++class ToPy a where+ toPy :: a -> IO Py.SomeObject++class FromPy a where+ fromPy :: Py.SomeObject -> IO a++data PyCastException = PyCastException String+ deriving (Show)++instance Exception PyCastException where+ displayException (PyCastException typename) =+ "FromPy could not cast to " ++ typename++easyToPy+ :: Py.Object c+ => (a -> IO c) -- ^ python to- conversion, e.g. Py.toFloat+ -> a -- ^ haskell type being converted+ -> IO Py.SomeObject+easyToPy f = fmap Py.toObject . f++easyFromPy+ :: (Py.Concrete b, Typeable c)+ => (b -> IO c) -- ^ python from- conversion, e.g. Py.fromFloat+ -> Proxy c -- ^ proxy for the type being converted to+ -> Py.SomeObject -- ^ python object to cast from+ -> IO c+easyFromPy conversion typename obj = do+ casted <- Py.cast obj+ case casted of+ Nothing -> throwIO $ PyCastException (show $ typeRep typename)+ Just x -> conversion x++instance ToPy Integer where+ toPy = easyToPy Py.toInteger++instance FromPy Integer where+ fromPy = easyFromPy Py.fromInteger Proxy++instance ToPy Double where+ toPy = easyToPy Py.toFloat++instance FromPy Double where+ fromPy = easyFromPy Py.fromFloat Proxy++instance ToPy Text where+ toPy = easyToPy Py.toUnicode++instance FromPy Text where+ fromPy = easyFromPy Py.fromUnicode Proxy++instance ToPy Char where+ toPy = easyToPy Py.toUnicode . T.singleton++instance FromPy Char where+ fromPy c = T.head <$> easyFromPy Py.fromUnicode Proxy c++instance ToPy String where+ toPy = easyToPy Py.toUnicode . T.pack++instance FromPy String where+ fromPy s = T.unpack <$> easyFromPy Py.fromUnicode Proxy s++instance (FromPy a, FromPy b) => FromPy (a, b) where+ fromPy val = do+ [pyA, pyB] <- easyFromPy Py.fromTuple Proxy val+ a <- fromPy pyA+ b <- fromPy pyB+ pure (a, b)++instance (FromPy a, FromPy b, FromPy c) => FromPy (a, b, c) where+ fromPy val = do+ [pyA, pyB, pyC] <- easyFromPy Py.fromTuple Proxy val+ a <- fromPy pyA+ b <- fromPy pyB+ c <- fromPy pyC+ pure (a, b, c)++instance FromPy a => FromPy (Maybe a) where+ fromPy val = do+ isNone <- Py.isNone val+ if isNone+ then pure Nothing+ else Just <$> fromPy val++instance ToPy a => ToPy (Maybe a) where+ toPy Nothing = Py.none+ toPy (Just a) = toPy a++instance FromPy a => FromPy [a] where+ fromPy val = do+ list <- easyFromPy Py.fromList Proxy val+ mapM fromPy list++instance ToPy a => ToPy [a] where+ toPy val = do+ list <- mapM toPy val+ Py.toObject <$> Py.toList list++instance FromPy () where+ fromPy _ = pure ()