packages feed

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 view
@@ -0,0 +1,188 @@+# Haskell CPython C bindings [![Build Status](https://travis-ci.org/zsedem/haskell-cpython.svg?branch=main)](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 ()