Skip to content

Commit e663e32

Browse files
authored
Add From/ToPy instances for Bool and 2-tuples (#8)
2 parents 7885dc2 + c8e32d4 commit e663e32

File tree

9 files changed

+180
-1
lines changed

9 files changed

+180
-1
lines changed

cbits/python.c

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,45 @@ PyObject *inline_py_function_wrapper(PyCFunction fun, int flags) {
4343
return f;
4444
}
4545

46+
int inline_py_unpack_iterable(PyObject *iterable, int n, PyObject **out) {
47+
// Fill out with NULL. This way we can call XDECREF on them
48+
for(int i = 0; i < n; i++) {
49+
out[i] = NULL;
50+
}
51+
// Initialize iterator
52+
PyObject* iter = PyObject_GetIter( iterable );
53+
if( PyErr_Occurred() ) {
54+
return -1;
55+
}
56+
if( !PyIter_Check(iter) ) {
57+
goto err_iter;
58+
}
59+
// Fill elements
60+
for(int i = 0; i < n; i++) {
61+
out[i] = PyIter_Next(iter);
62+
if( NULL==out[i] ) {
63+
goto err_elem;
64+
}
65+
}
66+
// End of iteration
67+
PyObject* end = PyIter_Next(iter);
68+
if( NULL != end || PyErr_Occurred() ) {
69+
goto err_end;
70+
}
71+
return 0;
72+
//----------------------------------------
73+
err_end:
74+
Py_XDECREF(end);
75+
err_elem:
76+
for(int i = 0; i < n; i++) {
77+
Py_XDECREF(out[i]);
78+
}
79+
err_iter:
80+
Py_DECREF(iter);
81+
return -1;
82+
}
83+
84+
4685
void inline_py_free_capsule(PyObject* py) {
4786
PyMethodDef *meth = PyCapsule_GetPointer(py, NULL);
4887
// HACK: We want to release wrappers created by wrapper. It

include/inline-python.h

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
#define INLINE_PY_ERR_COMPILE 1
1111
#define INLINE_PY_ERR_EVAL 2
1212

13+
14+
1315
// This macro checks for errors. If python exception is raised it
1416
// clear it and returns 1 otherwise retruns 0
1517
#define INLINE_PY_SIMPLE_ERROR_HANDLING() do { \
@@ -29,6 +31,20 @@ void inline_py_export_exception(
2931
char** p_msg
3032
);
3133

34+
// Unpack iterable into array of PyObjects. Iterable must contain
35+
// exactly N elements.
36+
//
37+
// On success returns 0 and fills `out` with N PyObjects
38+
//
39+
// On failure returns -1. Python exception is not cleared. It's
40+
// responsibility of caller to deal with it. Content of `out` is
41+
// undefined in this case.
42+
int inline_py_unpack_iterable(
43+
PyObject *iterable,
44+
int n,
45+
PyObject **out
46+
);
47+
3248
// Allocate python function object which carrries its own PyMethodDef.
3349
// Returns function object or NULL with error raised.
3450
//

inline-python.cabal

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ library test
8484
hs-source-dirs: test
8585
Exposed-modules:
8686
TST.Run
87+
TST.FromPy
8788

8889
test-suite inline-python-tests
8990
import: language

shell.nix

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
let
22
pkgs = import <nixpkgs> {};
33
py = pkgs.python3.withPackages (py_pkg: with py_pkg;
4-
[
4+
[ numpy
5+
matplotlib
56
]);
67
in
78
pkgs.mkShell {
@@ -10,4 +11,7 @@ pkgs.mkShell {
1011
pkg-config
1112
py
1213
];
14+
shellHook = ''
15+
export PYTHONHOME=${py}
16+
'';
1317
}

src/Python/Inline/Literal.hs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ module Python.Inline.Literal
1313
import Control.Exception
1414
import Control.Monad
1515
import Control.Monad.IO.Class
16+
import Control.Monad.Trans.Class
1617
import Control.Monad.Trans.Cont
1718
import Data.Int
1819
import Data.Word
@@ -28,6 +29,7 @@ import Language.C.Inline.Unsafe qualified as CU
2829
import Python.Types
2930
import Python.Internal.Types
3031
import Python.Internal.Eval
32+
import Python.Internal.Util
3133

3234

3335
----------------------------------------------------------------
@@ -143,6 +145,60 @@ instance ToPy Int where
143145
instance FromPy Int where
144146
basicFromPy = (fmap . fmap) fromIntegral . basicFromPy @Int64
145147

148+
-- TODO: Int may be 32 or 64 bit!
149+
-- TODO: Int{8,16,32} & Word{8,16,32}
150+
151+
instance ToPy Bool where
152+
basicToPy True = Py [CU.exp| PyObject* { Py_True } |]
153+
basicToPy False = Py [CU.exp| PyObject* { Py_False } |]
154+
155+
-- | Uses python's truthiness conventions
156+
instance FromPy Bool where
157+
basicFromPy p = Py $ do
158+
r <- [CU.block| int {
159+
int r = PyObject_IsTrue($(PyObject* p));
160+
PyErr_Clear();
161+
return r;
162+
} |]
163+
case r of
164+
0 -> pure $ Just False
165+
1 -> pure $ Just True
166+
_ -> pure $ Nothing
167+
168+
instance (ToPy a, ToPy b) => ToPy (a,b) where
169+
basicToPy (a,b) = do
170+
basicToPy a >>= \case
171+
NULL -> pure NULL
172+
p_a -> basicToPy b >>= \case
173+
NULL -> pure $ NULL
174+
p_b -> Py [CU.exp| PyObject* { PyTuple_Pack(2, $(PyObject* p_a), $(PyObject* p_b)) } |]
175+
176+
instance (FromPy a, FromPy b) => FromPy (a,b) where
177+
basicFromPy p_tup = evalContT $ do
178+
-- Unpack 2-tuple.
179+
p_args <- withPyAllocaArray 2
180+
unpack_ok <- liftIO [CU.exp| int {
181+
inline_py_unpack_iterable($(PyObject *p_tup), 2, $(PyObject **p_args))
182+
}|]
183+
-- We may want to extract exception to haskell side later
184+
liftIO [CU.exp| void { PyErr_Clear() } |]
185+
when (unpack_ok /= 0) $ abort $ pure Nothing
186+
-- Unpack 2-elements
187+
lift $ do
188+
p_a <- liftIO $ peekElemOff p_args 0
189+
p_b <- liftIO $ peekElemOff p_args 1
190+
let parse = basicFromPy p_a >>= \case
191+
Nothing -> pure Nothing
192+
Just a -> basicFromPy p_b >>= \case
193+
Nothing -> pure Nothing
194+
Just b -> pure $ Just (a,b)
195+
fini = liftIO [CU.block| void {
196+
Py_XDECREF( $(PyObject* p_a) );
197+
Py_XDECREF( $(PyObject* p_b) );
198+
} |]
199+
parse `finallyPy` fini
200+
201+
146202

147203
----------------------------------------------------------------
148204
-- Functions marshalling

src/Python/Internal/Types.hs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,21 @@ module Python.Internal.Types
1010
PyObject(..)
1111
, PyError(..)
1212
, Py(..)
13+
, finallyPy
1314
-- * inline-C
1415
, pyCtx
1516
-- * Patterns
1617
, pattern INLINE_PY_OK
1718
, pattern INLINE_PY_ERR_COMPILE
1819
, pattern INLINE_PY_ERR_EVAL
20+
, pattern NULL
1921
) where
2022

2123
import Control.Exception
2224
import Control.Monad.IO.Class
25+
import Data.Coerce
2326
import Data.Map.Strict qualified as Map
27+
import Foreign.Ptr
2428
import Foreign.ForeignPtr
2529
import Foreign.C.Types
2630
import Language.C.Types
@@ -50,6 +54,8 @@ newtype Py a = Py (IO a)
5054
deriving newtype (Functor,Applicative,Monad,MonadIO,MonadFail)
5155
-- See NOTE: [Python and threading]
5256

57+
finallyPy :: forall a b. Py a -> Py b -> Py a
58+
finallyPy = coerce (finally @a @b)
5359

5460
----------------------------------------------------------------
5561
-- inline-C
@@ -67,3 +73,8 @@ pattern INLINE_PY_OK, INLINE_PY_ERR_COMPILE, INLINE_PY_ERR_EVAL :: CInt
6773
pattern INLINE_PY_OK = 0
6874
pattern INLINE_PY_ERR_COMPILE = 1
6975
pattern INLINE_PY_ERR_EVAL = 2
76+
77+
78+
pattern NULL :: Ptr a
79+
pattern NULL <- ((== nullPtr) -> True) where
80+
NULL = nullPtr

src/Python/Internal/Util.hs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ withWCtring = withArray0 (CWchar 0) . map (fromIntegral . ord)
2525
withPyAlloca :: forall a r. Storable a => ContT r Py (Ptr a)
2626
withPyAlloca = coerce (alloca @a @r)
2727

28+
withPyAllocaArray :: forall a r. Storable a => Int -> ContT r Py (Ptr a)
29+
withPyAllocaArray = coerce (allocaArray @a @r)
30+
2831
withPyCString :: forall r. String -> ContT r Py CString
2932
withPyCString = coerce (withCString @r)
3033

test/TST/FromPy.hs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
{-# LANGUAGE QuasiQuotes #-}
2+
-- |
3+
module TST.FromPy (tests) where
4+
5+
import Test.Tasty
6+
import Test.Tasty.HUnit
7+
import Python.Inline
8+
import Python.Inline.QQ
9+
10+
tests :: TestTree
11+
tests = testGroup "FromPy"
12+
[ testGroup "Int"
13+
[ testCase "Int->Int" $ eq @Int (Just 1234) =<< [pye| 1234 |]
14+
, testCase "Double->Int" $ eq @Int Nothing =<< [pye| 1234.25 |]
15+
, testCase "None->Int" $ eq @Int Nothing =<< [pye| None |]
16+
]
17+
, testGroup "Double"
18+
[ testCase "Int->Double" $ eq @Double (Just 1234) =<< [pye| 1234 |]
19+
, testCase "Double->Double" $ eq @Double (Just 1234.25) =<< [pye| 1234.25 |]
20+
, testCase "None->Double" $ eq @Double Nothing =<< [pye| None |]
21+
]
22+
, testGroup "Bool"
23+
[ testCase "True->Bool" $ eq @Bool (Just True) =<< [pye| True |]
24+
, testCase "False->Bool" $ eq @Bool (Just False) =<< [pye| False |]
25+
, testCase "None->Bool" $ eq @Bool (Just False) =<< [pye| None |]
26+
-- FIXME: Names leak!
27+
, testCase "Exception" $ do
28+
[pymain|
29+
class Bad:
30+
def __bool__(self):
31+
raise Exception("Bad __bool__")
32+
|]
33+
eq @Bool Nothing =<< [pye| Bad() |]
34+
-- Segfaults if exception is not cleared
35+
[py_| 1+1 |]
36+
]
37+
, testGroup "Tuple2"
38+
[ testCase "(2)->2" $ eq @(Int,Bool) (Just (2,True)) =<< [pye| (2,2) |]
39+
, testCase "[2]->2" $ eq @(Int,Bool) (Just (2,True)) =<< [pye| [2,2] |]
40+
, testCase "(1)->2" $ eq @(Int,Bool) Nothing =<< [pye| (1) |]
41+
, testCase "(3)->2" $ eq @(Int,Bool) Nothing =<< [pye| (1,2,3) |]
42+
, testCase "X->2" $ eq @(Int,Bool) Nothing =<< [pye| 2 |]
43+
]
44+
]
45+
46+
eq :: (Eq a, Show a, FromPy a) => Maybe a -> PyObject -> IO ()
47+
eq a p = assertEqual "fromPy: " a =<< fromPy p

test/exe/main.hs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@ module Main where
33
import Test.Tasty
44

55
import TST.Run
6+
import TST.FromPy
67
import Python.Inline
78

89
main :: IO ()
910
main = withPython $ defaultMain $ testGroup "PY"
1011
[ TST.Run.tests
12+
, TST.FromPy.tests
1113
]

0 commit comments

Comments
 (0)