packages feed

inline-python 0.2.1.0 → 0.3.0.0

raw patch · 22 files changed

+1533/−341 lines, 22 filesdep +deepseqdep ~basedep ~bytestringdep ~containersbuild-type:Customsetup-changedPVP ok

version bump matches the API change (PVP)

Dependencies added: deepseq

Dependency ranges changed: base, bytestring, containers, exceptions, stm, tasty, text, vector

API changes (from Hackage documentation)

+ Python.Inline.Async: PyAsyncCancelled :: PyAsyncCancelled
+ Python.Inline.Async: cancelPy :: PyAsync a -> IO ()
+ Python.Inline.Async: data PyAsync a
+ Python.Inline.Async: data PyAsyncCancelled
+ Python.Inline.Async: runPyAsync :: Py a -> IO (PyAsync a)
+ Python.Inline.Async: uninterruptibleCancelPy :: PyAsync a -> IO ()
+ Python.Inline.Async: waitPy :: PyAsync a -> STM a
+ Python.Inline.Async: waitPyCatch :: PyAsync a -> STM (Either SomeException a)
+ Python.Inline.Async: withPyAsync :: Py a -> (PyAsync a -> IO b) -> IO b
+ Python.Inline.Eval: PyQuoteFun :: !Code -> !DictBinder -> PyQuoteFun
+ Python.Inline.Eval: createModule :: Text -> PyQuote -> Py Module
+ Python.Inline.Eval: data PyQuoteFun
+ Python.Inline.Eval: evalPyFunction :: Namespace global => global -> PyQuoteFun -> Py PyObject
+ Python.Inline.Monad: basicNamespaceDict :: Namespace a => a -> Py (Ptr PyObject)
+ Python.Inline.Monad: class Monad m => MonadPy (m :: Type -> Type)
+ Python.Inline.Monad: class Namespace a
+ Python.Inline.Monad: evalM :: MonadPy m => PyQuote -> m PyObject
+ Python.Inline.Monad: evalPyFunctionM :: MonadPy m => PyQuoteFun -> m PyObject
+ Python.Inline.Monad: execM :: MonadPy m => PyQuote -> m ()
+ Python.Inline.Monad: instance Python.Inline.Monad.MonadPy Python.Internal.Types.Py
+ Python.Inline.Monad: liftPy :: MonadPy m => Py a -> m a
+ Python.Inline.Monad: withGlobals :: MonadPy m => (forall globals. Namespace globals => globals -> m a) -> m a
+ Python.Inline.Monad: withLocals :: MonadPy m => (forall locals. Namespace locals => locals -> m a) -> m a
+ Python.Inline.Monad.QQ: pySource :: FilePath -> Q Exp
+ Python.Inline.Monad.QQ: py_ :: QuasiQuoter
+ Python.Inline.Monad.QQ: pycode :: QuasiQuoter
+ Python.Inline.Monad.QQ: pye :: QuasiQuoter
+ Python.Inline.Monad.QQ: pyf :: QuasiQuoter
+ Python.Inline.Monad.QQ: pyfun :: QuasiQuoter
+ Python.Inline.Monad.QQ: pymain :: QuasiQuoter
+ Python.Inline.QQ: pySource :: FilePath -> Q Exp
+ Python.Inline.QQ: pyfun :: QuasiQuoter
- Python.Inline.Eval: [binder] :: PyQuote -> !DictBinder
+ Python.Inline.Eval: [binder] :: PyQuoteFun -> !DictBinder
- Python.Inline.Eval: [code] :: PyQuote -> !Code
+ Python.Inline.Eval: [code] :: PyQuoteFun -> !Code

Files

ChangeLog.md view
@@ -1,3 +1,33 @@+0.3.0.0 [2026.10.15]+--------------------+* Support for asynchronous execution added in module `Python.Inline.Async`.  It+  uses API modelled after `async` package. Such computations could be+  interrupted using `cancelPy` even when they're running python code or haskell+  callback/+* When threaded runtime is used `runPyInMain` could be reliably interrupted by+  asynchronous exceptions.+* Python module `inline_python` is now available. It contains exception types+  used by library: `AsyncCancelled` and `HaskellError` which wraps haskell+  exception from callback.+* `Python.Inline.Monad` and `Python.Inline.Monad.QQ` modules providing monadic+  API. It uses `MonadPy` type class which allows user code to carry around+  python dictionaries with global and local scopes.+* Package now uses `Custom` build type. It now supports configuring python using+  `python3-config` instead of `pkg-config` when `-fpython3-config` manual cabal+  flag is set. Default behavior is unchanged.+* Haskell exception raised in haskell callback will be rethrown if not caught by+  python instead of being converted to `PyError`.+* `evalPyFunction` is added to company of `eval` and `exec`. It uses new+  `PyQuoteFun` data type as parameter.+* `pySource` quasiquoter added. It loads python code from file. +* `createModule` added which gives standard way of defining python code.+* Bug fixes:+ - Memory leak in exception handling is fixed. Python exception object were never+   freed when exception propagated to haskell side.+ - `bindVar` and `codeFromString`, `codeFromText` violated memory safety but+   happened to work correctly when strings were created by TH. Consider upgrade+   if using them.+ 0.2.1.0 [2026.01.13] ---------------- * `From/ToPy` instance for `Integer`&`Natural` added.@@ -21,7 +51,7 @@ 0.1.1 [2025.02.13] ------------------ * Number of deadlocks in `runPyInMain` fixed:-  - It no longer deadlocks is exception is thrown+  - It no longer deadlocks if exception is thrown   - Nested calls no longer deadlock.   - Calling it from python callback. * `ToPy` instance added for `Py b`, `a -> Py b`, `a1 -> a2 -> Py b`
+ Setup.hs view
@@ -0,0 +1,79 @@+{-# LANGUAGE CPP                 #-}+{-# LANGUAGE DataKinds           #-}+{-# LANGUAGE ImportQualifiedPost #-}+{-# LANGUAGE OverloadedRecordDot #-}+{-# LANGUAGE OverloadedStrings   #-}+import Data.Either++import Distribution.Simple+import Distribution.Simple.Setup+import Distribution.Simple.LocalBuildInfo+import Distribution.PackageDescription+import Distribution.Types.BuildInfo+import Distribution.Types.PackageDescription+import Distribution.Types.CondTree+import Distribution.Types.ForeignLib+import Distribution.Types.UnqualComponentName+import Distribution.Utils.Path+import Distribution.Compat.Lens+import Distribution.Types.BuildInfo.Lens qualified as L++import Data.List (intercalate, nub)+import System.Process+++main :: IO ()+main = defaultMainWithHooks $ simpleUserHooks+  { confHook = patchedConfHook }++++patchedConfHook+  :: (GenericPackageDescription, HookedBuildInfo)+  -> ConfigFlags+  -> IO LocalBuildInfo+patchedConfHook (gpd, hbi) flags+  | Just True <- lookupFlagAssignment ("python3-config") $ configConfigurationsFlags flags+  = do cflags_raw  <- readProcess "python3-config" ["--cflags"] ""+       ldflags_raw <- readProcess "python3-config" ["--embed", "--ldflags"] ""+       -- Split flags+       let (inc_dirs,cflags) = partitionEithers+             [ case flag of+                 '-':'I':path -> Left  path+                 _            -> Right flag+             | flag <- tokenizeArguments cflags_raw+             ]+           (ldflags, (libs, lib_dirs)) = fmap partitionEithers $ partitionEithers+             [ case flag of+                 '-':'l':path -> Right (Left  path)+                 '-':'L':path -> Right (Right path)+                 _            -> Left flag+             | flag <- tokenizeArguments ldflags_raw+             ]+       let tweakLib lib = case lib.libName of+             LMainLibName -> lib & L.ccOptions    %~ (++ cflags)+                                 & L.includeDirs  %~ (++ (toSymb <$> inc_dirs))+                                 & L.ldOptions    %~ (++ ldflags)+                                 & L.extraLibs    %~ (++ libs)+                                 & L.extraLibDirs %~ (++ (toSymb <$> lib_dirs))+             _            -> lib+       confHook simpleUserHooks+         ( gpd { condLibrary = (fmap . fmap) tweakLib (condLibrary  gpd) }+         , hbi+         ) flags+  | otherwise = do+      confHook simpleUserHooks (gpd, hbi) flags++tokenizeArguments :: String -> [String]+tokenizeArguments = words++----------------------------------------------------------------+-- Compatibility++#if MIN_VERSION_Cabal_syntax(3,14,0)+toSymb :: String -> SymbolicPathX 'AllowAbsolute from to+toSymb = makeSymbolicPath+#else+toSymb :: String -> String+toSymb = id+#endif
cbits/python.c view
@@ -210,3 +210,181 @@     PyLong_AsNativeBytes(p, buf, size, -1); #endif }+++static Py_tss_t key_py_async = Py_tss_NEEDS_INIT;++void inline_py_init_state(void *stack) {+    int r = PyThread_tss_set(&key_py_async, stack);+    if( 0 != r ) {+        fprintf(stderr, "inline-python: fatal error: setting thread local storage failed\n");+        exit(1);+    }+}++void inline_py_free_state(void) {+    int r = PyThread_tss_set(&key_py_async, NULL);+    if( 0 != r ) {+        fprintf(stderr, "inline-python: fatal error: setting thread local storage failed\n");+        exit(1);+    }+}++void* inline_py_get_state(void) {+    return PyThread_tss_get(&key_py_async);+}+++void inline_py_initialize(void) {+    int r = PyThread_tss_create(&key_py_async);+    if( 0 != r ) {+        fprintf(stderr, "inline-python: fatal error: Failed to initialized thread local storage\n");+        exit(1);+    }+}++void inline_py_clear_error(void) {+    static PyObject* globals = NULL;+    static PyObject* locals  = NULL;+    static PyObject* code    = NULL;+    if( NULL == code ) {+        globals = PyDict_New();+        locals  = PyDict_New();+        code    = Py_CompileString("None", "<interactive>", Py_eval_input);+    }+    // None is immortal. No need to decrement counter+    PyEval_EvalCode(code, globals, locals);+    PyErr_Clear();+}+++// ================================================================+// inline_python module+// ================================================================++PyObject* (*inline_py_haskell_error_repr)(void*);+PyObject* (*inline_py_haskell_error_tyrepr)(void*);++PyObject* inline_py_AsyncCancelled() {+    static PyObject* AsyncCancelled = 0;+    if( AsyncCancelled == 0 ) {+        AsyncCancelled = PyErr_NewException("inline_python.AsyncCancelled", PyExc_BaseException, 0);+    }+    return AsyncCancelled;+}++typedef struct {+    PyBaseExceptionObject obj;+    void *exception_stableptr;+} HaskellError;++static void haskell_error_dealloc(PyObject *op) {+    HaskellError *self = (HaskellError*) op;+    hs_free_stable_ptr(self->exception_stableptr);+    Py_TYPE(self)->tp_free(self);+}++static PyObject* haskell_error_repr(PyObject *op) {+    HaskellError *self = (HaskellError*) op;+    PyObject* exc_repr = inline_py_haskell_error_repr(self->exception_stableptr);+    PyObject* exc_ty   = inline_py_haskell_error_tyrepr(self->exception_stableptr);+    PyObject* repr     = PyUnicode_FromFormat("<inline_python.HaskellError: %S: %S>", exc_ty, exc_repr);+    Py_DECREF(exc_repr);+    Py_DECREF(exc_ty);+    return repr;+}++static PyTypeObject HaskellError_Type = {+    PyVarObject_HEAD_INIT(NULL, 0)+    .tp_name      = "inline_python.HaskellError",+    .tp_basicsize = sizeof(HaskellError),+    .tp_flags     = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_DISALLOW_INSTANTIATION,+    .tp_doc       = PyDoc_STR("Wrapper for a haskell exception object"),+    .tp_dealloc   = haskell_error_dealloc,+    .tp_repr      = haskell_error_repr+};+++PyObject* inline_py_HaskellError() {+    static int initialized = 0;+    if( 0 == initialized ) {+        if( PyType_Ready(&HaskellError_Type) != 0) {+            // It should success always and we don't have any reasonable+            // way of handing error.+            exit(1);+        }+        initialized = 1;+    }+    return (PyObject*)&HaskellError_Type;+}++PyObject* inline_py_HaskellError_create(void* exc_ptr) {+    PyTypeObject *base = HaskellError_Type.tp_base;+    PyObject *args     = PyTuple_New(0);+    // Call __new__+    PyObject *obj = base->tp_new(&HaskellError_Type, args, NULL);+    if( !obj ) {+        goto err;+    }+    // Call __init__+    if( 0 != base->tp_init(obj, args, NULL) ) {+        goto err;+    }+    // Set custom fields+    HaskellError* h_err = (HaskellError*)obj;+    h_err->exception_stableptr = exc_ptr;+    Py_DECREF(args);+    return obj;+err:+    Py_DECREF(args);+    return NULL;+}++void* inline_py_HaskellError_get_stableptr(PyObject* err) {+    HaskellError *h_err = (HaskellError*) err;+    return h_err->exception_stableptr;+}++++static PyMethodDef inline_python_methods[] = {+    {NULL, NULL, 0, NULL}+};++static int inline_python_module_exec(PyObject *m) {+    // Initialize module only once+    static int initialized = 0;+    if( initialized ) {+        return 0;+    }+    initialized = 1;+    //+    if (PyModule_AddObjectRef(m, "AsyncCancelled", inline_py_AsyncCancelled()) < 0) {+        return -1;+    }+    //+    HaskellError_Type.tp_base = (PyTypeObject*)PyExc_Exception;+    HaskellError_Type.tp_new  = NULL;+    if (PyModule_AddObjectRef(m, "HaskellError", inline_py_HaskellError()) < 0) {+        return -1;+    }+    return 0;+}++static PyModuleDef_Slot inline_python_module_slots[] = {+    {Py_mod_exec, inline_python_module_exec},+    {0, NULL}+};++static struct PyModuleDef inline_python_module = {+    .m_base    = PyModuleDef_HEAD_INIT,+    .m_name    = "inline_python",+    .m_size    = 0,+    .m_slots   = inline_python_module_slots,+    .m_methods = inline_python_methods,+};++PyMODINIT_FUNC PyInit_inline_python(void)+{+    return PyModuleDef_Init(&inline_python_module);+}
include/inline-python.h view
@@ -17,6 +17,8 @@ #define Py_IsFinalizing(x) 0 #endif +// General initialization of internal on C side+void inline_py_initialize(void);   // ================================================================@@ -81,3 +83,53 @@     void*     buf,     size_t    size );++++// ================================================================+// runPyAsync & Async exceptions+// ================================================================++// Force clear error possibly raised by PyThreadState_SetAsyncExc+//+// Ordinary PyErr_Clear doesn't work. One needed python interpreter to+// perform somce work in order to see raised exception.+void inline_py_clear_error(void);++// Initialize thread local storage as used by runPyAsync+void inline_py_init_state(void *stack);++// Delete thread local storage used by runPyAsync+void inline_py_free_state(void);++// Return stable pointer to stack from TLS+void* inline_py_get_state(void);++++// ================================================================+// inline_python module+// ================================================================++// Function pointers to be set from haskell side++// String representation of an exception+extern PyObject* (*inline_py_haskell_error_repr)(void*);+// String representation of exception type+extern PyObject* (*inline_py_haskell_error_tyrepr)(void*);+++PyMODINIT_FUNC PyInit_inline_python(void);++// Obtain type for async exception.+PyObject* inline_py_AsyncCancelled();++// Obtain type for wrapper for haskell exceptions.+PyObject* inline_py_HaskellError();++// Create python object wrapping haskell exception+PyObject* inline_py_HaskellError_create(void* exc_ptr);++// Get StablePtr held by exception. Python object must be of type+// HaskellError+void* inline_py_HaskellError_get_stableptr(PyObject* err);
inline-python.cabal view
@@ -1,8 +1,8 @@ Cabal-Version:  3.0-Build-Type:     Simple-+Build-Type:     Custom+                 Name:           inline-python-Version:        0.2.1.0+Version:        0.3.0.0 Synopsis:       Python interpreter embedded into haskell. Description:   This package embeds python interpreter into haskell program and@@ -24,13 +24,25 @@   py/bound-vars.py  Tested-With:-  GHC == 9.2.8   GHC == 9.4.8   GHC == 9.6.7   GHC == 9.8.4   GHC == 9.10.2   GHC == 9.12.2+  GHC == 9.14.1 ++custom-setup+  setup-depends: base          >=4.15 && <5+               , Cabal         >=3.0  && <4+               , Cabal-syntax  >=3.6  && <4+               , process       >=1.6  && <2++flag python3-config+    Description: Use python3-config instead of pkg-config+    Manual:  True+    Default: False+ source-repository head   type:     git   location: http://github.com/Shimuuar/inline-python@@ -55,8 +67,9 @@ ---------------------------------------------------------------- Library   import:            language-  Build-Depends:     base             >=4.14 && <5+  Build-Depends:     base             >=4.15 && <5                    , primitive        >=0.6.2+                   , deepseq          >=1.4                    , vector           >=0.13.2                    , containers       >=0.5                    , process@@ -72,13 +85,18 @@   include-dirs:      include   c-sources:         cbits/python.c   cc-options:        -g -Wall-  pkgconfig-depends: python3-embed >= 3.10+  -- Else we use python3-config in Setup.hs+  if !flag(python3-config)+    pkgconfig-depends: python3-embed >= 3.10   --   Exposed-modules:     Python.Inline+    Python.Inline.Async+    Python.Inline.Eval     Python.Inline.Literal+    Python.Inline.Monad+    Python.Inline.Monad.QQ     Python.Inline.QQ-    Python.Inline.Eval     Python.Inline.Types   Other-modules:     Python.Internal.CAPI@@ -88,53 +106,53 @@     Python.Internal.Types     Python.Internal.Util ------------------------------------------------------------------library test-  import:           language+common tests   Default-Extensions:     QuasiQuotes+  hs-source-dirs:   test   build-depends:    base                   , inline-python                   , tasty                >=1.2                   , tasty-hunit          >=0.10                   , tasty-quickcheck     >=0.10+                  , stm                   , quickcheck-instances >=0.3.33                   , exceptions                   , containers                   , vector                   , bytestring                   , text-  hs-source-dirs:   test-  Exposed-modules:+++-- Running tests using several threads does very good job at finding threading+-- bugs. Especially deadlocks+test-suite inline-python-tests+  import:           language, tests+  type:             exitcode-stdio-1.0+  Ghc-options:      -threaded -rtsopts -with-rtsopts=-N2+  main-is:          exe/main.hs+  Other-modules:     TST.Run     TST.ToPy     TST.FromPy     TST.Callbacks     TST.Roundtrip     TST.Util---- Running tests using several threads does very good job at finding threading--- bugs. Especially deadlocks-test-suite inline-python-tests-  import:           language-  type:             exitcode-stdio-1.0-  Ghc-options:      -threaded -with-rtsopts=-N2-  hs-source-dirs:   test/exe-  main-is:          main.hs-  build-depends:    base-                  , inline-python-                  , inline-python:test-                  , tasty+    TST.Module  test-suite inline-python-tests1-  import:           language+  import:           language, tests   type:             exitcode-stdio-1.0-  hs-source-dirs:   test/exe-  main-is:          main.hs-  build-depends:    base-                  , inline-python-                  , inline-python:test-                  , tasty+  Ghc-options:      -rtsopts+  main-is:          exe/main.hs+  Other-modules:+    TST.Run+    TST.ToPy+    TST.FromPy+    TST.Callbacks+    TST.Roundtrip+    TST.Util+    TST.Module  benchmark pysmall   import:           language
src/Python/Inline.hs view
@@ -105,6 +105,7 @@ -- -- Here's list of common problems and solutions and workarounds. --+-- -- 1. __@inline-python@ cannot find libraries__ -- -- @inline-python@ may look for modules in wrong place. Set@@ -112,9 +113,67 @@ -- right way. -- ----- 2. __Linker error in GHCi__+-- 2. __Picking correct python interpreter__ --+-- Python's version @inline-python@ uses is determined by @libpython3@+-- it's linked with. This is decided when package is build. Normally+-- @pkg-config@ is used and this means using whatever distribution is+-- shipping. It's also possible to use @python3-config@ program by+-- specifying manual cabal flag @-fpython3-config@.+--+-- If it's desired that @inline-python@ should use python installed by+-- conda\/uv\/etc @inline-python@ should be built in environment where+-- desired python version is active and use @-fpython3-config@ flag.+-- This could be done by adding following to cabal.project:+--+-- > constraints: inline-python -fpython3-config+--+--+-- 3. __Linker error in GHCi__+-- -- Attempting to import library using C extensions from ghci may--- result in linker failing to find symbols from @libpython@ like--- @PyFloat_Type@ or some other. Only known workaround is to set--- @LD_PRELOAD=/path/to/libpython3.XX.so@ environment variable.+-- result in linker failing to find symbols from @libpython3@ like+-- @PyFloat_Type@ or some other. There are multiples known+-- workarounds. @libpython3.XX.so@ should be one @inline-python@ was+-- built with.+--+-- - export @LD_PRELOAD=\/path\/to\/libpython3.XX.so@ environment variable. This+-- works fine most of the time but it will also impact programs called from+-- your repl (e.g. using 'process').+--+-- - you can load the relevant symbol only in the current process space using+-- +-- > System.Posix.DynamicLinker.dlopen "/path/to/libpython3.XX.so" [System.Posix.DynamicLinker.RTLD_NOW, System.Posix.DynamicLinker.RTLD_GLOBAL]+--+-- Note that in ghci, you don't need to explicitly import+-- "System.Posix.DynamicLinker" to run this command. If you use this+-- extensively in your project, it is recommended to add that in your+-- @.ghci@, global or local, either as an unconditionally executed+-- command, or as a macro, such as:+--+-- > :def setupPythonDLL \_ -> "" <$ System.Posix.DynamicLinker.dlopen "path/to//lib/libpython3.so" [System.Posix.DynamicLinker.RTLD_NOW, System.Posix.DynamicLinker.RTLD_GLOBAL]@+--+-- 4. __GHCi reload__+--+-- Even if you reload ghci (using @:reload@), the python environment stays+-- initialised. As a result, the python modules imported using 'Python.Inline.QQ.pymain' won't+-- be reloaded, which is often source of confusion.+--+-- You can use python's @importlib.reload(m)@. For example:+--+-- >+-- >         [pymain|+-- >           import json+-- >           import importlib+-- >           import pandas+-- >           import mylib+-- >+-- >           # This force reloads mylib+-- >           importlib.reload(mylib)+-- >       |]+-- +--+-- Note that there is a performance drawback, the side effects of 'import' are+-- redone and python does not give much guarantee about what is happening here.+-- Use it with caution. We recommend using @importlib.reload@ only during+-- development and not in production.
+ src/Python/Inline/Async.hs view
@@ -0,0 +1,22 @@+-- |+-- Asynchronous computation using python. Its API is modelled after+-- @async@ package. It evaluates python on separate OS thread so it's+-- more heavyweight than 'Python.Inline.runPy'. But it's possible to+-- properly interrupt running computation with 'cancelPy' or to use+-- 'withPyAsync' to ensure that async computation properly terminated.+--+-- Since arbitrary IO is available either in @Py@ via @liftIO@ or in+-- haskell callbacks from python code. It's possible to use haskell+-- concurrency primitives to communicate with python thread.+module Python.Inline.Async+  ( PyAsync+  , PyAsyncCancelled(..)+  , runPyAsync+  , withPyAsync+  , waitPy+  , waitPyCatch+  , cancelPy+  , uninterruptibleCancelPy+  ) where++import Python.Internal.Eval
src/Python/Inline/Eval.hs view
@@ -1,5 +1,6 @@-{-# LANGUAGE QuasiQuotes     #-}-{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes       #-}+{-# LANGUAGE TemplateHaskell   #-} -- | -- Interface to python's @eval@ and @exec@ which gives programmer -- control over local and global variables.@@ -7,8 +8,11 @@   ( -- * Python execution     eval   , exec+  , evalPyFunction+  , createModule     -- * Source code   , PyQuote(..)+  , PyQuoteFun(..)   , Code   , codeFromText   , codeFromString@@ -22,8 +26,9 @@   , Module(..)   ) where -import Data.ByteString.Unsafe     qualified as BS+import Data.ByteString            qualified as BS import Data.Text                  (Text)+import Data.Text                  qualified as T import Data.Text.Encoding         qualified as T import Language.C.Inline          qualified as C import Language.C.Inline.Unsafe   qualified as CU@@ -33,7 +38,6 @@ import Python.Internal.Program import Python.Inline.Literal - ---------------------------------------------------------------- C.context (C.baseCtx <> pyCtx) C.include "<inline-python.h>"@@ -46,7 +50,7 @@   -> a    -- ^ Variable value   -> DictBinder bindVar name a = DictBinder $ \p_dict -> runProgram $ do-  p_key <- progIOBracket $ BS.unsafeUseAsCString (T.encodeUtf8 name)+  p_key <- progIOBracket $ BS.useAsCString (T.encodeUtf8 name)   p_obj <- takeOwnership =<< progPy (throwOnNULL =<< basicToPy a)   progPy $ do     r <- Py [CU.block| int {@@ -56,4 +60,26 @@     case r of       0 -> pure ()       _ -> mustThrowPyError+++-- | Create python module from given quote. This function creates new+--   module using @importlib@ machinery and executes quote in context+--   of this module.+createModule+  :: Text+  -> PyQuote+  -> Py Module+createModule name quote = do+  -- It would be nice to use quasiquotes but linker chokes on them.+  -- We have to this hard way+  mdl <- Module <$> evalPyFunction Temp PyQuoteFun+    { code   = codeFromText $ T.unlines+        [ "def _inline_python_(name):"+        , "    import types"+        , "    return types.ModuleType(name)"+        ]+    , binder = bindVar "name" name+    }+  mdl <$ exec mdl mdl quote+ 
src/Python/Inline/Literal.hs view
@@ -14,6 +14,8 @@   , fromPy'   ) where +import Control.Concurrent+import Control.Concurrent.STM import Control.Exception           (evaluate) import Control.Monad import Control.Monad.Catch@@ -44,6 +46,7 @@ import Foreign.Ptr import Foreign.C.Types import Foreign.Storable+import Foreign.StablePtr import Foreign.Marshal.Alloc     (alloca,mallocBytes) import Foreign.Marshal.Utils     (copyBytes) import GHC.Float                 (float2Double, double2Float)@@ -357,7 +360,7 @@       -1 -> do         neg <- takeOwnership            <=< progPy-             $ throwOnNULL =<< Py [CU.exp| PyObject* { PyNumber_Negative( $(PyObject *p) ) } |]+             $ throwOnNULL =<< Py [C.exp| PyObject* { PyNumber_Negative( $(PyObject *p) ) } |]         BA.ByteArray ba <- progIO $ decodePositiveInteger neg         pure $ GHC.Num.Integer.IN ba       -- Unreachable@@ -449,7 +452,7 @@ -- | Uses python's truthiness conventions instance FromPy Bool where   basicFromPy p = do-    r <- Py [CU.exp| int { PyObject_IsTrue($(PyObject* p)) } |]+    r <- Py [C.exp| int { PyObject_IsTrue($(PyObject* p)) } |]     checkThrowPyError     pure $! r /= 0 @@ -465,7 +468,7 @@   basicFromPy p_tup = runProgram $ do     -- Unpack 2-tuple.     p_args    <- withPyAllocaArray 2-    unpack_ok <- progIO [CU.exp| int {+    unpack_ok <- progIO [C.exp| int {       inline_py_unpack_iterable($(PyObject *p_tup), 2, $(PyObject **p_args))       }|]     progPy $ do checkThrowPyError@@ -490,7 +493,7 @@   basicFromPy p_tup = runProgram $ do     -- Unpack 3-tuple.     p_args    <- withPyAllocaArray 3-    unpack_ok <- progIO [CU.exp| int {+    unpack_ok <- progIO [C.exp| int {       inline_py_unpack_iterable($(PyObject *p_tup), 3, $(PyObject **p_args))       }|]     progPy $ do checkThrowPyError@@ -518,7 +521,7 @@   basicFromPy p_tup = runProgram $ do     -- Unpack 3-tuple.     p_args    <- withPyAllocaArray 4-    unpack_ok <- progIO [CU.exp| int {+    unpack_ok <- progIO [C.exp| int {       inline_py_unpack_iterable($(PyObject *p_tup), 4, $(PyObject **p_args))       }|]     progPy $ do checkThrowPyError@@ -558,7 +561,7 @@ -- | Will accept any iterable instance (FromPy a) => FromPy [a] where   basicFromPy p_list = do-    p_iter <- Py [CU.block| PyObject* {+    p_iter <- Py [C.block| PyObject* {       PyObject* iter = PyObject_GetIter( $(PyObject *p_list) );       if( PyErr_Occurred() ) {           PyErr_Clear();@@ -604,7 +607,7 @@             NULL -> mustThrowPyError             p_k  -> flip finally (decref p_k) $ basicToPy v >>= \case               NULL -> mustThrowPyError-              p_v  -> Py [CU.exp| int { PyDict_SetItem($(PyObject *p_dict), $(PyObject* p_k), $(PyObject *p_v)) }|] >>= \case+              p_v  -> Py [C.exp| int { PyDict_SetItem($(PyObject *p_dict), $(PyObject* p_k), $(PyObject *p_v)) }|] >>= \case                 0 -> loop xs                 _ -> nullPtr <$ decref p_v       loop $ Map.toList dct@@ -615,7 +618,7 @@                  throwM BadPyType     p_iter -> foldPyIterable p_iter       (\m p -> do k <- basicFromPy p-                  v <- Py [CU.exp| PyObject* { PyDict_GetItem($(PyObject* p_dct), $(PyObject *p)) }|] >>= \case+                  v <- Py [C.exp| PyObject* { PyDict_GetItem($(PyObject* p_dct), $(PyObject *p)) }|] >>= \case                     NULL -> throwM BadPyType                     p_v  -> basicFromPy p_v                   pure $! Map.insert k v m)@@ -671,19 +674,20 @@ vectorFromPy :: (VG.Vector v a, FromPy a) => Ptr PyObject -> Py (v a) {-# INLINE vectorFromPy #-} vectorFromPy p_seq = do-  len <- Py [CU.exp| long long { PySequence_Size($(PyObject* p_seq)) } |]+  len <- Py [C.exp| long long { PySequence_Size($(PyObject* p_seq)) } |]   when (len < 0) $ do     Py [C.exp| void { PyErr_Clear() } |]     throwM BadPyType   -- Read data into vector   buf <- MVG.generateM (fromIntegral len) $ \i -> do     let i_c = fromIntegral i-    Py [CU.exp| PyObject* { PySequence_GetItem($(PyObject* p_seq), $(long long i_c)) } |] >>= \case+    Py [C.exp| PyObject* { PySequence_GetItem($(PyObject* p_seq), $(long long i_c)) } |] >>= \case       NULL -> mustThrowPyError       p    -> basicFromPy p `finally` decref p   VG.unsafeFreeze buf  vectorToPy :: (VG.Vector v a, ToPy a) => v a -> Py (Ptr PyObject)+{-# INLINE vectorToPy #-} vectorToPy vec = runProgram $ do   p_list <- takeOwnership =<< checkNull (Py [CU.exp| PyObject* { PyList_New($(long long n_c)) } |])   progPy $ do@@ -924,8 +928,28 @@  -- | Execute haskell callback function pyCallback :: Program (Ptr PyObject) (Ptr PyObject) -> IO (Ptr PyObject)-pyCallback io = callbackEnsurePyLock $ unsafeRunPy $ ensureGIL $ runProgram io `catch` convertHaskell2Py+pyCallback io+  = mask_+  $ withCallbackStack+  $ unsafeRunPy+  $ ensureGIL+  $ runProgram io `catch` convertHaskell2Py +withCallbackStack :: IO a -> IO a+withCallbackStack = bracket ini fini . const where+  ini = [CU.exp| void* { inline_py_get_state() } |] >>= \case+    NULL -> return NULL+    ptr  -> do+      tid   <- myThreadId+      stack <- deRefStablePtr $ castPtrToStablePtr ptr+      atomically $ modifyTVar' stack (tid:)+      return ptr+  fini NULL = return ()+  fini ptr  = do+    stack <- deRefStablePtr $ castPtrToStablePtr ptr+    atomically $ modifyTVar' stack (drop 1)++ -- | Load argument from python object for haskell evaluation loadArg   :: FromPy a@@ -940,7 +964,7 @@     Left  OutOfRange -> oops     Left  e          -> throwM e     where-      oops = Py [CU.block| PyObject* {+      oops = Py [C.block| PyObject* {         char err[256];         sprintf(err, "Failed to decode function argument %i of %li", $(int i)+1, $(int64_t tot));         PyErr_SetString(PyExc_TypeError, err);@@ -959,7 +983,7 @@   loadArg p i tot  raiseBadNArgs :: CInt -> Int64 -> Py (Ptr PyObject)-raiseBadNArgs expected got = Py [CU.block| PyObject* {+raiseBadNArgs expected got = Py [C.block| PyObject* {   char err[256];   sprintf(err, "Function takes exactly %i arguments (%li given)", $(int expected), $(int64_t got));   PyErr_SetString(PyExc_TypeError, err);
+ src/Python/Inline/Monad.hs view
@@ -0,0 +1,76 @@+-- |+module Python.Inline.Monad+  ( -- * Type classes+    Namespace(..)+  , MonadPy(..)+  , evalM+  , execM+  , evalPyFunctionM+  ) where++import Python.Internal.Eval+import Python.Internal.Types++-- | Monad which can carry dictionaries or object of global and local+--   python variables around. It's expected that it's some variant of+--   wrapper around 'Py'.+--+--   For @Py@ global variables are 'Main' and local are 'Temp'+--+--  @since 0.3+class Monad m => MonadPy m where+  -- | Lift @Py@ computation into given monad.+  liftPy :: Py a -> m a+  -- | Provide set of global variables. CPS style is used to avoid+  --   specifying type of global used by monad (and allow picking it+  --   at runtime).+  withGlobals+    :: (forall globals. Namespace globals => globals -> m a)+    -> m a+  -- | Same for local variables+  withLocals+    :: (forall locals. Namespace locals => locals -> m a)+    -> m a++instance MonadPy Py where+  liftPy = id+  withGlobals f = f Main+  withLocals  f = f Temp+++-- | Analog of 'eval' which uses globals and locals carried by 'MonadPy'+--+--  @since 0.3+evalM+  :: (MonadPy m)+  => PyQuote -- ^ Source code+  -> m PyObject+evalM q =+  withGlobals  $ \globals ->+    withLocals $ \locals  ->+      liftPy $ eval globals locals q++-- | Analog of 'exec' which uses globals and locals carried by+--  'MonadPy'+--+--  @since 0.3+execM+  :: (MonadPy m)+  => PyQuote -- ^ Source code+  -> m ()+execM q =+  withGlobals  $ \globals ->+    withLocals $ \locals  ->+      liftPy $ exec globals locals q++-- | Analog of 'evalPyFunction' which uses globals and locals carried by+--  'MonadPy'.+--+--  @since 0.3+evalPyFunctionM+  :: (MonadPy m)+  => PyQuoteFun -- ^ Source code+  -> m PyObject+evalPyFunctionM q =+  withGlobals $ \globals ->+    liftPy $ evalPyFunction globals q
+ src/Python/Inline/Monad/QQ.hs view
@@ -0,0 +1,94 @@+{-# LANGUAGE TemplateHaskell #-}+-- |+-- These quasiquotes are analogous to ones defined in+-- "Python.Inline.QQ" but they produce splices which are polymorphic+-- in 'MonadPy'. For 'Py' in particular they behave identically. +module Python.Inline.Monad.QQ+  ( -- * Evalution in @Py@ monad+    pymain+  , py_+  , pye+  , pyf+    -- * Creation of @PyQuote@+  , pycode+  , pyfun+  , pySource+  ) where+++import Language.Haskell.TH.Quote++import Python.Internal.EvalQQ+import Python.Internal.Eval+import Python.Inline.Monad+import Python.Inline.QQ (pycode, pyfun, pySource)++++-- | Evaluate sequence of python statements. It uses python's @exec@.+--   Both global and local scope for this quasiquoter are global+--   variables for 'MonadPy'+--+--   It creates value of type @MonadPy m => m ()@+pymain :: QuasiQuoter+pymain = QuasiQuoter+  { quoteExp  = \txt -> [|+     withGlobals $ \globals -> +       liftPy $ exec globals globals $(expQQ Exec txt)+     |]+  , quotePat  = error "quotePat"+  , quoteType = error "quoteType"+  , quoteDec  = error "quoteDec"+  }++-- | Evaluate sequence of python statements. Global and local+--   variables for this quasiquoter are determined by 'MonadPy'+--   instance.+--+--   It creates value of type @MonadPy m => m ()@+py_ :: QuasiQuoter+py_ = QuasiQuoter+  { quoteExp  = \txt -> [|+      withGlobals $ \globals ->+        withLocals $ \locals -> +          liftPy $ exec globals locals $(expQQ Exec txt)+      |]+  , quotePat  = error "quotePat"+  , quoteType = error "quoteType"+  , quoteDec  = error "quoteDec"+  }++-- | Evaluate single python expression. It only accepts single+--   expressions same as python's @eval@. Its globals are variables+--   are determined by 'MonadPy' instance.+--+--   This quote creates object of type @MonadPy m => m PyObject@+pye :: QuasiQuoter+pye = QuasiQuoter+  { quoteExp  = \txt -> [|+      withGlobals $ \globals ->+        withLocals $ \locals -> +          liftPy $ eval globals locals $(expQQ Eval txt)+      |]+  , quotePat  = error "quotePat"+  , quoteType = error "quoteType"+  , quoteDec  = error "quoteDec"+  }++-- | Another quasiquoter which works around that sequence of python+--   statements doesn't have any value associated with it. Content of+--   quasiquote is function body. So to get value out of it one must+--   call return. Its globals are determined by 'MonadPy' instance. +--   Just like python function it always creates new scope.+--+--   This quote creates object of type @Py PyObject@+pyf :: QuasiQuoter+pyf = QuasiQuoter+  { quoteExp  = \txt -> [|+      withGlobals $ \globals ->+        liftPy $ evalPyFunction globals $(expQQ Fun txt)+      |]+  , quotePat  = error "quotePat"+  , quoteType = error "quoteType"+  , quoteDec  = error "quoteDec"+  }
src/Python/Inline/QQ.hs view
@@ -30,25 +30,38 @@ -- >         do_that() -- >         |] ----- If control over python's global and local variables is--- required. APIs from "Python.Inline.Eval" should be used instead.+--+-- == Variable scope+--+-- Python has two copes: global and local variables. Both are simply+-- @dict[str,Any]@. Quasiquoters use different dictionaries for+-- globals and locals. If tighter control over variables scope is+-- required APIs from "Python.Inline.Eval" should be used instead. module Python.Inline.QQ-  ( pymain+  ( -- * Evalution in @Py@ monad+    pymain   , py_   , pye   , pyf+    -- * Creation of @PyQuote@+    -- $PyQuote   , pycode+  , pyfun+  , pySource   ) where  import Language.Haskell.TH.Quote+import Language.Haskell.TH.Syntax qualified as TH  import Python.Internal.EvalQQ import Python.Internal.Eval+import Python.Internal.Types  --- | Evaluate sequence of python statements. It works in the same way---   as python's @exec@. All module imports and all variables defined---   in this quasiquote will be visible to later quotes.+-- | Evaluate sequence of python statements. It uses python's @exec@.+--   Both global and local scope for this quasiquoter are variables of+--   @\__main__@ module. Any variables including imported modules will+--   remain visible to later quasiquotes. -- --   It creates value of type @Py ()@ pymain :: QuasiQuoter@@ -59,9 +72,11 @@   , quoteDec  = error "quoteDec"   } --- | Evaluate sequence of python statements. All module imports and---   all variables defined in this quasiquote will be discarded and---   won't be visible in later quotes.+-- | Evaluate sequence of python statements. Global variables for this+--   quasiquoter are one defined in @\__main__@ module and locals use+--   newly allocated dictionary. It will be discarded after execution+--   so variables defined in this quasiquote are visible only inside+--   of it. -- --   It creates value of type @Py ()@ py_ :: QuasiQuoter@@ -73,7 +88,8 @@   }  -- | Evaluate single python expression. It only accepts single---   expressions same as python's @eval@.+--   expressions same as python's @eval@. Its globals are variables in+--   @\__main__@ module and locals are new dictionary same as in @py_@. -- --   This quote creates object of type @Py PyObject@ pye :: QuasiQuoter@@ -85,21 +101,30 @@   }  -- | Another quasiquoter which works around that sequence of python---   statements doesn't have any value associated with it.  Content of+--   statements doesn't have any value associated with it. Content of --   quasiquote is function body. So to get value out of it one must---   call return+--   call return. Its globals are variables in @\__main__@ module and+--   locals are new dictionary same as in @py_@.+--+--   This quote creates object of type @Py PyObject@ pyf :: QuasiQuoter pyf = QuasiQuoter-  { quoteExp  = \txt -> [| evaluatorPyf $(expQQ Fun txt) |]+  { quoteExp  = \txt -> [| evalPyFunction Main $(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'+-- $PyQuote -----   It creates value of type @PyQuote@+-- 'PyQuote' wraps python code and bould haskell variables. It could+-- be evaluated using 'Python.Inline.Eval.exec',+-- 'Python.Inline.Eval.eval', 'Python.Inline.Eval.evalPyFunction'.+++-- | Create quote of python code. It captures haskell variables in the+--   same way as rest of quasiquotes and creates value of type+--   'Python.Inline.Eval.PyQuote'. -- --   @since 0.2@ pycode :: QuasiQuoter@@ -109,3 +134,30 @@   , quoteType = error "quoteType"   , quoteDec  = error "quoteDec"   }++-- | Create quote of python code suitable for use with+--   'Python.Inline.Eval.exec'+--+--   It creates value of type @PyQuoteFun@+--+--   @since 0.3@+pyfun :: QuasiQuoter+pyfun = QuasiQuoter+  { quoteExp  = \txt -> expQQ Fun txt+  , quotePat  = error "quotePat"+  , quoteType = error "quoteType"+  , quoteDec  = error "quoteDec"+  }++-- | Create value of type 'PyQuote' from file. Created quote doesn't+--   capture any variables.+--+--   @since 0.3+pySource :: FilePath -> TH.Q TH.Exp+pySource path = do+  TH.addDependentFile path+  [| PyQuote+     ($(TH.lift =<< TH.runIO (codeFromString <$> readFile path)))+     mempty+   |]+
src/Python/Internal/CAPI.hs view
@@ -4,6 +4,7 @@ -- Thin wrappers over C API module Python.Internal.CAPI   ( decref+  , xdecref   , incref     -- * Simple wrappers   , basicNewDict@@ -27,8 +28,11 @@   decref :: Ptr PyObject -> Py ()-decref p = Py [CU.exp| void { Py_DECREF($(PyObject* p)) } |]+decref p = Py [C.exp| void { Py_DECREF($(PyObject* p)) } |] +xdecref :: Ptr PyObject -> Py ()+xdecref p = Py [C.exp| void { Py_XDECREF($(PyObject* p)) } |]+ incref :: Ptr PyObject -> Py () incref p = Py [CU.exp| void { Py_INCREF($(PyObject* p)) } |] @@ -39,7 +43,7 @@ basicNewSet = Py [CU.exp| PyObject* { PySet_New(NULL) } |]  basicGetIter :: Ptr PyObject -> Py (Ptr PyObject)-basicGetIter p = Py [CU.exp| PyObject* { PyObject_GetIter( $(PyObject *p)) } |]+basicGetIter p = Py [C.exp| PyObject* { PyObject_GetIter( $(PyObject *p)) } |]  basicIterNext :: Ptr PyObject -> Py (Ptr PyObject) basicIterNext p = Py [C.exp| PyObject* { PyIter_Next($(PyObject* p)) } |]@@ -50,7 +54,7 @@   :: Ptr PyObject -- ^ Function object   -> Ptr PyObject -- ^ Keywords. Must be dictionary   -> Py (Ptr PyObject)-basicCallKwdOnly fun kwd = Py [CU.block| PyObject* {+basicCallKwdOnly fun kwd = Py [C.block| PyObject* {   PyObject* args = PyTuple_Pack(0);   PyObject* res  = PyObject_Call($(PyObject *fun), args, $(PyObject *kwd));   Py_DECREF(args);
src/Python/Internal/Eval.hs view
@@ -2,12 +2,12 @@ {-# LANGUAGE QuasiQuotes               #-} {-# LANGUAGE RecordWildCards           #-} {-# LANGUAGE TemplateHaskell           #-}+{-# OPTIONS_GHC -Wno-orphans #-} -- | -- Evaluation of python expressions. module Python.Internal.Eval   ( -- * Locks     ensurePyLock-  , callbackEnsurePyLock     -- * Initialization   , initializePython   , finalizePython@@ -16,6 +16,15 @@   , runPy   , runPyInMain   , unsafeRunPy+    -- ** Async+  , PyAsync+  , PyAsyncCancelled(..)+  , waitPy+  , waitPyCatch+  , cancelPy+  , uninterruptibleCancelPy+  , runPyAsync+  , withPyAsync     -- * GC-related   , newPyObject     -- * C-API wrappers@@ -40,13 +49,15 @@   , unsafeWithCode   , eval   , exec+  , evalPyFunction     -- * Debugging   , debugPrintPy   ) where  import Control.Concurrent import Control.Concurrent.STM-import Control.Exception         (interruptible)+import Control.Exception         (interruptible,evaluate)+import Control.DeepSeq import Control.Monad import Control.Monad.Catch import Control.Monad.IO.Class@@ -54,11 +65,12 @@ import Data.Maybe import Data.Function import Data.ByteString.Unsafe    qualified as BS+import Data.Typeable import Foreign.Concurrent        qualified as GHC import Foreign.Ptr import Foreign.ForeignPtr+import Foreign.StablePtr import Foreign.C.Types-import Foreign.C.String import Foreign.Marshal.Array import Foreign.Storable import System.Environment@@ -96,37 +108,43 @@ -- Haskell has two runtimes. Single threaded one doesn't cause any -- troubles and won't be discussed further. Multithreaded one -- implement N-M threading and schedules N green thread on M OS--- threads as it see fit.------ One could think that running python code in bound threads and--- making sure that GIL is held would suffice. It doesn't. Doing so--- would quickly results in deadlock. Exact reason for that is not--- understood.------ Another problem is GHC may schedule two threads each running python--- code on same capability. They won't have any problems taking GIL--- and will run concurrently stepping on each other's toes.+-- threads as GHC RTS sees fit. ----- Only way to solve this problem is to introduce another lock on--- haskell side. It's visible to haskell RTS so we won't get deadlocks--- and it makes sure that only one haskell thread interacts with--- python at a time.+-- Runtime may migrate haskell threads between OS threads freely so+-- consecutive calls to python may happen in different threads. This+-- doesn't seem to cause problems so far. In similar way several+-- threads may interleave calls to python in single OS+-- thread. Hopefully it won't cause problems either. --+-- Pre 0.3 version had a global lock allowing only single runPy to+-- execute at any time. --+-- For uses where serious concurrency is required runPyAsync machinery+-- should be used. See NOTE [Py Async] for details.++++-- NOTE: [Main thread]+-- ~~~~~~~~~~~~~~~~~~~ -- -- Also python designate thread in which python interpreter was -- initialized as a main thread. It has special status for example--- some libraries may run only in main thread (e.g. tkinter). But if--- we don't take special precautions we won't know which thread it--- is.---+-- some libraries may run only in main thread (e.g. tkinter). In+-- single threaded runtime everything is simple: we one only one+-- thread anyway. --+-- In multithreaded one we start dedicated thread using forkOS and use+-- standard tools for interacting with it. This requires asynchrony+-- and all complications that come with it. ----- There's of course question how well python threading interacts with--- haskell. No one knows, probably it won't work well.+-- Execution is protected by haskell mutex. Only one thread can+-- evaluate code at time. This is critical for preserving correctness+-- for cancelling main thread. While we hold lock no one else can+-- perform operations with main thread.   + -- NOTE: [GC] -- ~~~~~~~~~~ --@@ -149,23 +167,49 @@ -- NOTE: [Interrupting python] -- ~~~~~~~~~~~~~~~~~~~~~~~~~~~ ----- Being able to interrupt python when haskell exception arrives is--- surely nice. However it's difficult and comes with tradeoffs.+-- Interrupting program that mixes python and haskell code is in fact+-- very difficult. ----- First of all call must be done in a separate thread otherwise--- there's no one to catch exception and to something. This also means--- that python calls made using plain FFI are not interruptible.+--  + Haskell code cannot receive exceptions while in foreign call. ----- In addition python's ability to notify other threads are limited:+--  + Haskell callback from python created new lightweight+--    thread. Thus we cannot interrupt callback thread since we need+--    to know its thread ID. -- --  + `Py_SetInterrupt` plain doesn't work. It uses signal which trips --    up haskell RTS as well. -----  + `PyThreadState_SetAsyncExc` could be use but it requires special---    setup from thread being interrupted.+--  + `PyThreadState_SetAsyncExc` uses OS thread id (or something+--    similar) as a key. So we must execute code in a bound thread and+--    be sure that no other haskell thread (except callbacks) uses it.+--+-- Together this means it's only possible to interrupt python when+-- it's called in dedicated OS thread. Such as created by `runPyAsync`+-- or in main thread. To do this we need thread ID as used by python.+--+-- To interrupt haskell, whether thread we spawned or any callback+-- we'll have to maintain stack of thread IDs somehow. Obvoiusly+-- such stack has to be done by callback.+--+-- And even if we do have stack we can't reliably interrupt callbacks+-- due to asynchrony. We may look at stack just before callbacks TID+-- is pushed onto it. In that case we'll try interrupt parent+-- thread. Or we can use its value just before TID is popped. In that+-- case we'll interrupt thread that's about to stop or stopped+-- already.+--+-- So it seems only way of dealing with this problem is to try to kill+-- thread on top of stack and whenever new thread appear on top of+-- stack. +--+--+-- As for runPy it seems there's simply no way to forcefully interrupt+-- computation. So it's not interruptible.   ++ ---------------------------------------------------------------- -- Lock and global state ----------------------------------------------------------------@@ -184,15 +228,17 @@   = NotInitialized     -- ^ Initialization is not done. Initial state.   | InInitialization-    -- ^ Interpreter is being initialized.+    -- ^ Interpreter is being initialized. This state is required in+    --   case initialization is started from different threads.   | InitFailed     -- ^ Initialization was attempted but failed for whatever reason.   | Running1     -- ^ Interpreter is running. We're using single threaded RTS   | RunningN !(Chan (Ptr PyObject))              !(MVar EvalReq)-             !ThreadId-             !ThreadId+             !ThreadId   -- Haskell ID of main thread+             !PyThreadId -- Python ID of main thread+             !ThreadId   -- GC thread ID     -- ^ Interpreter is running. We're using multithreaded RTS   | InFinalization     -- ^ Interpreter is being finalized.@@ -211,68 +257,39 @@ data PyLock   = LockUninialized     -- ^ There's no interpreter and lock does not exist.-  | LockUnlocked-    -- ^ Lock could be taked-  | Locked !ThreadId [ThreadId]-    -- ^ Python is locked by given thread. Lock could be taken multiple-    --   times-  | LockedByGC-    -- ^ Python is locked by GC thread.+  | LockReady !(TVar Int) !(TMVar ())+    -- ^ Interpreter is properly initialized and we track number of+    --   threads running python. Same thread may take lock multiple+    --   times: e.g. nested runPy.+    --+    --   Second parameter is mutex for main. We allow only single+    --   request in flight.   | LockFinalized     -- ^ Python interpreter shut down. Taking lock is not possible-  deriving Show  -- | Execute code ensuring that python lock is held by current thread. ensurePyLock :: IO a -> IO a-ensurePyLock action = do-  tid <- myThreadId-  bracket_ (atomically $ acquireLock tid)-           (atomically $ releaseLock tid)-           action---- | Retake lock regardless of thread which hold lock. Lock must be---   already taken. Caller must make sure that thread holding lock is---   block for duration of action.------   This is very unsafe. It must be used only in callbacks from---   python to haskell-callbackEnsurePyLock :: IO a -> IO a-callbackEnsurePyLock action = do-  tid <- myThreadId-  bracket_ (atomically $ grabLock tid)-           (atomically $ releaseLock tid)-           action-+ensurePyLock = bracket_+  (atomically acquireLock)+  (atomically releaseLock) -acquireLock :: ThreadId -> STM ()-acquireLock tid = readTVar globalPyLock >>= \case+acquireLock :: STM ()+acquireLock = readTVar globalPyLock >>= \case   LockUninialized -> throwSTM PythonNotInitialized   LockFinalized   -> throwSTM PythonIsFinalized-  LockedByGC      -> retry-  LockUnlocked    -> writeTVar globalPyLock $ Locked tid []-  Locked t xs-    | t == tid  -> writeTVar globalPyLock $ Locked t (t : xs)-    | otherwise -> retry+  LockReady n _   -> modifyTVar' n succ -grabLock :: ThreadId -> STM ()-grabLock tid = readTVar globalPyLock >>= \case+releaseLock :: STM ()+releaseLock = readTVar globalPyLock >>= \case   LockUninialized -> throwSTM PythonNotInitialized   LockFinalized   -> throwSTM PythonIsFinalized-  LockedByGC      -> retry-  LockUnlocked    -> writeTVar globalPyLock $ Locked tid []-  Locked t xs     -> writeTVar globalPyLock $ Locked tid (t : xs)+  LockReady n _   -> modifyTVar' n pred -releaseLock :: ThreadId -> STM ()-releaseLock tid = readTVar globalPyLock >>= \case-  LockUninialized -> throwSTM PythonNotInitialized-  LockFinalized   -> throwSTM PythonIsFinalized-  LockUnlocked    -> throwSTM $ PyInternalError "releaseLock: releasing LockUnlocked"-  LockedByGC      -> throwSTM $ PyInternalError "releaseLock: releasing LockedByGC"-  Locked t xs-    | t /= tid  -> throwSTM $ PyInternalError "releaseLock: releasing  wrong lock"-    | otherwise -> writeTVar globalPyLock $! case xs of-        []    -> LockUnlocked-        t':ts -> Locked t' ts+ensureInit :: IO ()+ensureInit = readTVarIO globalPyLock >>= \case+  LockUninialized -> throwM PythonNotInitialized+  LockFinalized   -> throwM PythonIsFinalized+  LockReady{}     -> pure ()   @@ -286,8 +303,8 @@ initializePython :: IO () -- See NOTE: [Python and threading] initializePython = [CU.exp| int { Py_IsInitialized() } |] >>= \case-  0 | rtsSupportsBoundThreads -> runInBoundThread $ doInializePython-    | otherwise               -> doInializePython+  0 | rtsSupportsBoundThreads -> runInBoundThread $ doInitializePython+    | otherwise               -> doInitializePython   _ -> pure ()  -- | Destroy python interpreter.@@ -304,7 +321,7 @@     Py_Finalize();     } |]   -- We need to call Py_Finalize on main thread-  RunningN _ lock_eval _ tid_gc -> checkLock $ do+  RunningN _ lock_eval _ _ tid_gc -> checkLock $ do     killThread tid_gc     resp <- newEmptyMVar     putMVar lock_eval $ StopReq resp@@ -313,12 +330,11 @@     checkLock action = readTVar globalPyLock >>= \case       LockUninialized -> throwSTM $ PyInternalError "finalizePython LockUninialized"       LockFinalized   -> throwSTM $ PyInternalError "finalizePython LockFinalized"-      Locked{}        -> retry-      LockedByGC      -> retry-      LockUnlocked    -> do-        writeTVar globalPyLock  LockFinalized-        writeTVar globalPyState Finalized-        pure action+      LockReady n _   -> readTVar n >>= \case+        0 -> do writeTVar globalPyLock  LockFinalized+                writeTVar globalPyState Finalized+                pure action+        _ -> retry  -- | Bracket which ensures that action is executed with properly --   initialized interpreter@@ -326,8 +342,8 @@ withPython = bracket_ initializePython finalizePython  -doInializePython :: IO ()-doInializePython = do+doInitializePython :: IO ()+doInitializePython = do   -- First we need to grab global python lock on haskell side   join $ atomically $ do     readTVar globalPyState >>= \case@@ -340,8 +356,10 @@       NotInitialized   -> do         writeTVar globalPyState InInitialization         let fini st = atomically $ do+              n         <- newTVar 0+              main_lock <- newTMVar ()               writeTVar globalPyState $ st-              writeTVar globalPyLock  $ LockUnlocked+              writeTVar globalPyLock  $ LockReady n main_lock         pure $           (mask_ $ if             -- On multithreaded runtime create bound thread to make@@ -351,56 +369,69 @@                 lock_eval <- newEmptyMVar                 -- Main thread                 tid_main <- forkOS $ mainThread lock_init lock_eval-                takeMVar lock_init >>= \case-                  True  -> pure ()-                  False -> throwM PyInitializationFailed+                tid_py   <- takeMVar lock_init >>= \case+                  Just tid -> pure tid+                  Nothing  -> throwM PyInitializationFailed                 -- GC thread                 gc_chan <- newChan                 tid_gc  <- forkOS $ gcThread gc_chan-                fini $ RunningN gc_chan lock_eval tid_main tid_gc+                fini $ RunningN gc_chan lock_eval tid_main tid_py tid_gc             -- Nothing special is needed on single threaded RTS             | otherwise -> do-                doInializePythonIO >>= \case+                doInitializePythonIO >>= \case                   True  -> pure ()                   False -> throwM PyInitializationFailed                 fini Running1           ) `onException` atomically (writeTVar globalPyState InitFailed)  -- This action is executed on python's main thread-mainThread :: MVar Bool -> MVar EvalReq -> IO ()+mainThread :: MVar (Maybe PyThreadId) -> MVar EvalReq -> IO () mainThread lock_init lock_eval = do-  r_init <- doInializePythonIO-  putMVar lock_init r_init-  case r_init of-    False -> pure ()-    True  -> mask_ $ fix $ \loop ->-      (takeMVar lock_eval `catch` (\InterruptMain -> pure HereWeGoAgain)) >>= \case-        EvalReq py resp -> do-          res <- (Right <$> runPy py) `catch` (pure . Left)-          putMVar resp res-          loop-        StopReq resp -> do-          [C.block| void {-            PyGILState_Ensure();-            Py_Finalize();-            } |]-          putMVar resp ()-        HereWeGoAgain -> loop+  doInitializePythonIO >>= \case+    False -> putMVar lock_init Nothing+    True  -> do+      putMVar lock_init . Just =<< getPyThreadID+      mask_ $ fix $ \loop -> do+        -- Here we discard any late PyAsyncCancelled exceptions+        (takeMVar lock_eval `catch` (\PyAsyncCancelled -> pure HereWeGoAgain)) >>= \case+          EvalReq py resp alive tid_stack -> do+            let action = withAsyncInitTLS tid_stack $ unsafeRunPy $ ensureGIL $ do+                  -- We must clear any error python indication. It+                  -- could be leftover from interrupting previous+                  -- evaluation+                  Py $ [CU.exp| void { inline_py_clear_error() } |]+                  Py $ putMVar alive True+                  py+            res <- try action+              `finally` uninterruptibleMask_ (modifyMVar_ alive (\_ -> pure False))+            putMVar resp res+            loop+          StopReq resp -> do+            [C.block| void {+              PyGILState_Ensure();+              inline_py_clear_error();+              Py_Finalize();+              } |]+            putMVar resp ()+          HereWeGoAgain -> loop  -doInializePythonIO :: IO Bool-doInializePythonIO = do+doInitializePythonIO :: IO Bool+doInitializePythonIO = do   -- FIXME: I'd like more direct access to argv   argv0 <- getProgName   argv  <- getArgs   let n_argv = fromIntegral $ length argv + 1-  -- FIXME: For some reason sys.argv is initialized incorrectly. No-  --        easy way to debug. Will do for now+  hask_err_repr   <- wrapReprFromStablePtr haskellErrorRepr+  hask_err_tyrepr <- wrapReprFromStablePtr haskellErrorTyRepr   r <- evalContT $ do     p_argv0  <- ContT $ withWCString argv0     p_argv   <- traverse (ContT . withWCString) argv     ptr_argv <- ContT $ withArray (p_argv0 : p_argv)     liftIO [C.block| int {+      // Set global constants+      inline_py_haskell_error_repr   = $(PyObject* (*hask_err_repr)(void*));+      inline_py_haskell_error_tyrepr = $(PyObject* (*hask_err_tyrepr)(void*));       // Now fill config       PyStatus status;       PyConfig cfg;@@ -420,12 +451,31 @@       if( PyStatus_Exception(status) ) {           goto error;       };+      // Make builting module visible to python+      PyImport_AppendInittab("inline_python", PyInit_inline_python);       // Initialize interpreter       status = Py_InitializeFromConfig(&cfg);       if( PyStatus_Exception(status) ) {           goto error;       };       PyConfig_Clear(&cfg);+      // This is hack for python<=3.11.+      //+      // Somehow we may end up in stet where thread ID of main thread is not equal+      // to main thread's one. Importing threading seems to fix it. However exact+      // reason for such behavior is unknown+      if( PY_MINOR_VERSION <= 11 ) {+          PyObject *threading = PyImport_ImportModule("threading");+          if( PyErr_Occurred() ) {+              PyErr_Clear();+          }+      }+      // Initialize internals. We also need to import module with our internals+      inline_py_initialize();+      PyObject *inline_python = PyImport_ImportModule("inline_python");+      if( PyErr_Occurred() ) {+          PyErr_Clear();+      }       // Release GIL so other threads may take it       PyEval_SaveThread();       return 0;@@ -436,23 +486,54 @@       } |]   return $! r == 0 +haskellErrorRepr :: Ptr () -> IO (Ptr PyObject)+haskellErrorRepr ptr = unsafeRunPy $ runProgram $ do+  SomeException err <- progIO $ deRefStablePtr $ castPtrToStablePtr ptr+  -- We need to make sure that we evaluated string so that we won't+  -- leak exceptions+  repr  <- progIO $ evaluate $ force $ show err+  p_str <- withPyWCString repr+  progIO [CU.exp| PyObject* { PyUnicode_FromWideChar($(wchar_t *p_str), -1) } |] +haskellErrorTyRepr :: Ptr () -> IO (Ptr PyObject)+haskellErrorTyRepr ptr = unsafeRunPy $ runProgram $ do+  SomeException err <- progIO $ deRefStablePtr $ castPtrToStablePtr ptr+  -- We need to make sure that we evaluated string so that we won't+  -- leak exceptions+  repr  <- progIO $ evaluate $ force $ show $ typeOf err+  p_str <- withPyWCString repr+  progIO [CU.exp| PyObject* { PyUnicode_FromWideChar($(wchar_t *p_str), -1) } |]++type FunWrapper a = a -> IO (FunPtr a)++foreign import ccall "wrapper" wrapReprFromStablePtr+  :: FunWrapper (Ptr () -> IO (Ptr PyObject))+++ ---------------------------------------------------------------- -- Running Py monad ---------------------------------------------------------------- +-- | Request that we send to main thread data EvalReq-  = forall a. EvalReq (Py a) (MVar (Either SomeException a))+  = forall a. EvalReq (Py a) (MVar (Either SomeException a)) (MVar Bool) (TVar [ThreadId])+    -- ^ Request to run code in main thread   | StopReq (MVar ())+    -- ^ Stop evaluation   | HereWeGoAgain+    -- ^ Dummy request. Do nothing -data InterruptMain = InterruptMain-  deriving stock    Show-  deriving anyclass Exception --- | Execute python action. It will take and hold global lock while---   code is executed. Python exceptions raised during execution are---   converted to haskell exception 'PyError'.+-- | Execute python action. This is simplest executor with lowest+--   overhead but it comes with several caveats. When thread executes+--   python code it could not be interrupted since it's in foreign+--   call. Use 'runPyAsync' if you need ability to interrupt. Also+--   python uses GIL so only one thread can evaluate python code at+--   time.+--+--   Python exceptions raised during execution are converted to+--   haskell exception 'PyError'. runPy :: Py a -> IO a -- See NOTE: [Python and threading] runPy py@@ -463,67 +544,245 @@     -- it wasn't. Better than segfault isn't it?     go = ensurePyLock $ mask_ $ unsafeRunPy (ensureGIL py) --- | Same as 'runPy' but will make sure that code is run in python's---   main thread. It's thread in which python's interpreter was---   initialized. Some python's libraries may need that. It has higher---   call overhead compared to 'runPy'.++-- | This function executes python code on python's main thread. It's+--   OS thread in which interpreter was initialized and it has some+--   special status in python. Some libraries could only work when+--   called from main thread. It has higher call overhead compared to+--   'runPy' and only one haskell thread could be executing something+--   on main thread at time.+--+--   When executing on threaded runtime this function could be+--   interrupted by asynchronous exceptions. runPyInMain :: Py a -> IO a--- See NOTE: [Python and threading]+-- See NOTE: [Python and threading, Main thread] runPyInMain py   -- Multithreaded RTS   | rtsSupportsBoundThreads = do-      tid <- myThreadId-      bracket (acquireMain tid) fst snd+      py_tid <- getPyThreadID+      bracket (acquireMain py_tid) fst snd   -- Single-threaded RTS   | otherwise = runPy py   where-    acquireMain tid = atomically $ readTVar globalPyState >>= \case+    acquireMain py_tid = atomically $ readTVar globalPyState >>= \case       NotInitialized   -> throwSTM PythonNotInitialized       InitFailed       -> throwSTM PyInitializationFailed       Finalized        -> throwSTM PythonIsFinalized       InInitialization -> retry       InFinalization   -> retry       Running1         -> throwSTM $ PyInternalError "runPyInMain: Running1"-      RunningN _ eval_lock tid_main _ -> readTVar globalPyLock >>= \case+      RunningN _ eval_lock tid_main tid_main_py _ -> readTVar globalPyLock >>= \case         LockUninialized -> throwSTM PythonNotInitialized         LockFinalized   -> throwSTM PythonIsFinalized-        LockedByGC      -> retry-        -- We need to send closure to main python thread when we're grabbing lock.-        LockUnlocked    -> do-          writeTVar globalPyLock $ Locked tid_main []-          pure ( atomically (releaseLock tid_main)-               , evalInOtherThread tid_main eval_lock-               )-        -- 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-            -> retry-          | t == tid_main || (tid_main `elem` ts) -> do-              writeTVar globalPyLock $ Locked t (t : ts)-              pure ( atomically (releaseLock t)-                   , unsafeRunPy $ ensureGIL py-                   )+        LockReady _ main_lock+          -- We're on main thread. We can just run computation and not+          -- bother with incrementing thread counter. It's already+          -- incremented in outer scope+          | py_tid == tid_main_py -> pure ( pure ()+                                          , unsafeRunPy $ ensureGIL py+                                          )+          -- Otherwise we need to send closure to main thread for evaluation.+          -- We use mutex to make sure that only single request is executed           | otherwise -> do-              writeTVar globalPyLock $ Locked tid_main (t : ts)-              pure ( atomically (releaseLock tid_main)-                   , evalInOtherThread tid_main eval_lock+              takeTMVar main_lock+              acquireLock+              pure ( atomically (releaseLock >> putTMVar main_lock ())+                   , evalInOtherThread tid_main tid_main_py eval_lock                    )     ---    evalInOtherThread tid_main eval_lock = do-      r <- mask_ $ do resp <- newEmptyMVar-                      putMVar eval_lock $ EvalReq py resp-                      takeMVar resp `onException` throwTo tid_main InterruptMain+    evalInOtherThread tid_main tid_main_py eval_lock = do+      r <- mask_ $ do+        resp      <- newEmptyMVar+        alive     <- newEmptyMVar+        tid_stack <- newTVarIO []+        putMVar eval_lock $ EvalReq py resp alive tid_stack+        takeMVar resp `onException` cancelPy PyAsync+          { asyncTID      = tid_main+          , asyncTidStack = tid_stack+          , asyncPyTID    = pure tid_main_py+          , asyncAlive    = alive+          , asyncWait     = retry -- Not used by cancelPy+          }       either throwM pure r - -- | Execute python action. This function is unsafe and should be only --   called in thread of interpreter. unsafeRunPy :: Py a -> IO a unsafeRunPy (Py io) = io  +----------------------------------------------------------------+-- Async running+---------------------------------------------------------------- +-- NOTE: [Py Async]+-- ~~~~~~~~~~~~~~~~+--+-- In order to run python threads concurrently and to be able to+-- cancel them we need to run python in dedicated thread. See NOTE+-- [Interrupting python] for details.+--+-- `forkOS` seems to provide such functionality although it's not+-- documented explicitly. It allows to provide async-inspired API for+-- interacting with python thread.+--+-- Most complicated part of API is interrupting python. [Interrupting+-- python] gives high level overview. Here more technical details:+--+--  + PyThreadState_SetAsyncExc doesn't queue exception. So it very+--    well may be noop if it finds no such thread. We have to repeat+--    interrupting python.+--+--  + forkOS may reuse OS thread. So we must not attempt interrupt if+--    thread is dead already.+++-- | Exception thrown to a thread doing async python computation. On+--   python side it corresponds to+--   @inline_python.AsyncCancelled@. Latter is automatically converted+--   to @PyAsyncCancelled@.+--+-- @since 0.3+data PyAsyncCancelled = PyAsyncCancelled+  deriving (Show, Eq)++instance Exception PyAsyncCancelled++-- | Handle to asynchronous python computation spawned by+--   'runPyAsync'. It's performed on separate OS thread. Use+--   'waitPy'\/'waitPyCatch' to obtain computation result.+--+-- @since 0.3+data PyAsync a = PyAsync+  { asyncTID      :: !ThreadId          -- Thread ID+  , asyncTidStack :: !(TVar [ThreadId]) -- Stack of callback thread ID+  , asyncPyTID    :: !(IO PyThreadId)   -- Thread ID used by python+  , asyncAlive    :: !(MVar Bool)       -- Holds True while thread is alive+  , asyncWait     :: STM (Either SomeException a)+  }++-- | Wait for result of asynchronous computation. If it threw an+--   exception it will be rethrown by @wait@.+--+-- @since 0.3+waitPy :: PyAsync a -> STM a+waitPy a = either throwSTM pure =<< a.asyncWait++-- | Wait for result of asynchronous computation. Exception thrown by+--   it will be returned as @Left@.+--+-- @since 0.3+waitPyCatch :: PyAsync a -> STM (Either SomeException a)+waitPyCatch = (.asyncWait)++-- | Execute python computation on dedicated OS thread.+--+-- @since 0.3+runPyAsync :: Py a -> IO (PyAsync a)+runPyAsync py = do+  ensureInit+  result    <- newEmptyTMVarIO+  tid_stack <- newTVarIO []+  py_tid_mv <- newEmptyMVar+  alive     <- newEmptyMVar+  -- Worker thread. We must modify liveliness MVar under+  -- uninterruptibleMask otherwise it could be interrupted and+  -- cancelPy will consider thread alive forever+  --+  -- We also clear dangling async python exception in case we have+  -- leftover from previous evaluation+  tid    <- forkOS $ mask_ $+    (do putMVar py_tid_mv =<< getPyThreadID+        a <- try+           $ withAsyncInitTLS tid_stack+           $ ensurePyLock+           $ unsafeRunPy+           $ ensureGIL $ do Py [CU.exp| void { inline_py_clear_error() } |]+                            Py $ putMVar alive True+                            py+        atomically $ putTMVar result a+    ) `finally` uninterruptibleMask_ (modifyMVar_ alive (\_ -> pure False))+  pure PyAsync+    { asyncTID      = tid+    , asyncTidStack = tid_stack+    , asyncPyTID    = readMVar py_tid_mv+    , asyncWait     = takeTMVar result+    , asyncAlive    = alive+    }++-- Initialize thread local storage for thread created by runPyAsync+withAsyncInitTLS :: TVar [ThreadId] -> IO a -> IO a+withAsyncInitTLS stack = bracket ini fini . const+  where+    ini = do+      s_ptr <- newStablePtr stack+      let ptr = castStablePtrToPtr s_ptr+      [CU.exp| void { inline_py_init_state($(void* ptr)) } |]+      pure s_ptr+    fini s_ptr = do+      [CU.exp| void { inline_py_free_state() } |]+      freeStablePtr s_ptr++++-- | Cancel execution of asynchronous computation. It throws+--   'PyAsyncCancelled' to haskell threads including any haskell+--   callbacks from python. Python will interrupted by asynchronously+--   raising @inline_python.AsyncCancelled@.+--+--   Note that python code generally is not written under assumption+--   that it could be smitten with exception at an absolutely any+--   moment. It could cause problems.+--+-- @since 0.3+cancelPy :: PyAsync a -> IO ()+cancelPy PyAsync{asyncTID=tid, asyncTidStack, asyncPyTID, asyncAlive} = do+  -- See NOTE: [Py Async], [Interrupting python]+  PyThreadId py_tid <- asyncPyTID+  -- Interrupting python. We must attempt interrupting only as long as+  -- async thread is alive. Else we may end up interrupting wrong+  -- thread.+  _ <- forkIO $ fix $ \loop -> do+    n <- withMVar asyncAlive $ \case+      False -> return 1+      True  -> [C.block| int {+        int gil = PyGILState_Ensure();+        int n   = PyThreadState_SetAsyncExc($(uint64_t py_tid), inline_py_AsyncCancelled());+        PyGILState_Release(gil);+        return n;+        }|]+    case n of+      0 -> do+        threadDelay 50 -- Avoid hammering interrupt too hard+        loop+      _ -> return ()+  -- Interrupting callbacks. We repeatedly try to kill every callbacks+  -- that appears on top of stack. It seems to be only working way.+  tid_kill_cb <- forkIO $ flip fix Nothing $ \loop t_old -> do+    t <- atomically $ readTVar asyncTidStack >>= \case+      []                    -> retry+      t:_ | Just t == t_old -> retry+          | otherwise       -> pure t+    _ <- forkIO $ throwTo t PyAsyncCancelled+    loop $ Just t+  -- Interrupt haskell+  throwTo tid PyAsyncCancelled+  killThread tid_kill_cb++-- | Variant of 'cancel' which isn't interruptible.+--+-- @since 0.3+uninterruptibleCancelPy :: PyAsync a -> IO ()+uninterruptibleCancelPy = uninterruptibleMask_ . cancelPy++-- | Create new OS thread and execute python code on it. Will use+--   'uninterruptibleCancel' after callback finishes execution.+--+-- @since 0.3+withPyAsync :: Py a -> (PyAsync a -> IO b) -> IO b+withPyAsync py = bracket (runPyAsync py) uninterruptibleCancelPy++ ---------------------------------------------------------------- -- GC-related functions ----------------------------------------------------------------@@ -535,9 +794,9 @@   fptr <- newForeignPtr_ p   GHC.addForeignPtrFinalizer fptr $     readTVarIO globalPyState >>= \case-      RunningN ch _ _ _  -> writeChan ch p-      Running1           -> singleThreadedDecrefCG p-      _                  -> pure ()+      RunningN ch _ _ _ _  -> writeChan ch p+      Running1             -> singleThreadedDecrefCG p+      _                    -> pure ()   pure $ PyObject fptr  -- | Thread doing garbage collection for python object in@@ -550,23 +809,19 @@ decrefGC p = join $ atomically $ readTVar globalPyLock >>= \case   LockUninialized -> pure $ pure ()   LockFinalized   -> pure $ pure ()-  LockedByGC      -> pure $ pure ()-  Locked{}        -> retry-  LockUnlocked    -> do-    writeTVar globalPyLock LockedByGC+  LockReady n _   -> do+    modifyTVar' n succ     pure $ do-      gcDecref p `finally` atomically (writeTVar globalPyLock LockUnlocked)+      gcDecref p `finally` atomically (modifyTVar' n pred)  singleThreadedDecrefCG :: Ptr PyObject -> IO () singleThreadedDecrefCG p = readTVarIO globalPyLock >>= \case   LockUninialized -> pure ()   LockFinalized   -> pure ()-  LockedByGC      -> gcDecref p-  Locked{}        -> gcDecref p-  LockUnlocked    -> gcDecref p+  LockReady{}     -> gcDecref p  gcDecref :: Ptr PyObject -> IO ()-gcDecref p = [CU.block| void {+gcDecref p = [C.block| void {   PyGILState_STATE st = PyGILState_Ensure();   Py_XDECREF( $(PyObject* p) );   PyGILState_Release(st);@@ -583,7 +838,7 @@   -- NOTE: We're cheating here and looking behind the veil.   --       PyGILState_STATE is defined as enum. Let hope it will stay   --       this way.-  gil_state <- Py [CU.exp| int { PyGILState_Ensure() } |]+  gil_state <- Py [C.exp| int { PyGILState_Ensure() } |]   action `finally` Py [CU.exp| void { PyGILState_Release($(int gil_state)) } |]  -- | Drop GIL temporarily@@ -594,9 +849,16 @@   --       this way.   st <- Py [CU.exp| PyThreadState* { PyEval_SaveThread() } |]   Py $ interruptible action-        `finally` [CU.exp| void { PyEval_RestoreThread($(PyThreadState *st)) } |]+        `finally` [C.exp| void { PyEval_RestoreThread($(PyThreadState *st)) } |]  +-- | Removes exception masking and releases GIL temporarily+instance MonadIO Py where+  liftIO = dropGIL . interruptible++getPyThreadID :: IO PyThreadId+getPyThreadID = PyThreadId <$> [CU.exp| uint64_t { PyThread_get_thread_ident() } |]+ ---------------------------------------------------------------- -- Conversion of exceptions ----------------------------------------------------------------@@ -605,35 +867,50 @@ --   NULL. convertHaskell2Py :: SomeException -> Py (Ptr PyObject) convertHaskell2Py err = Py $ do-  withCString ("Haskell exception: "++show err) $ \p_err -> do-    [CU.block| PyObject* {-      PyErr_SetString(PyExc_RuntimeError, $(char *p_err));-      return NULL;-      } |]+  s_ptr <- newStablePtr err+  let ptr = castStablePtrToPtr s_ptr+  [C.block| PyObject* {+    PyObject* exc = inline_py_HaskellError_create($(void* ptr));+    PyErr_SetObject(inline_py_HaskellError(), exc);+    Py_DECREF(exc);+    return NULL;+    } |]  -- | Convert python exception to haskell exception. Should only be --   called if there's unhandled python exception. Clears exception.-convertPy2Haskell :: Py PyException+convertPy2Haskell :: Py SomeException convertPy2Haskell = runProgram $ do   p_errors <- withPyAllocaArray @(Ptr PyObject) 3-  -- Fetch error indicator-  (p_type, p_value) <- progIO $ do-    [CU.block| void {-       PyObject **p = $(PyObject** p_errors);-       PyErr_Fetch(p, p+1, p+2);-       }|]-    p_type  <- peekElemOff p_errors 0-    p_value <- peekElemOff p_errors 1-    -- Traceback is not used ATM-    pure (p_type,p_value)-  -- Convert exception type and value to strings.+  -- Fetch error information+  progIO [CU.block| void {+    PyObject **p = $(PyObject** p_errors);+    PyErr_Fetch(p, p+1, p+2);+    }|]+  -- Fetch exception type.+  --+  -- NOTE: When we set exception using PyThreadState_SetAsyncExc this+  --       field remains NULL on python<=3.11. Thus we must use xdecref+  p_type  <- progPyBracket $ (Py $ peekElemOff p_errors 0) `bracket` decref+  p_value <- progPyBracket $ (Py $ peekElemOff p_errors 1) `bracket` xdecref+  _trace  <- progPyBracket $ (Py $ peekElemOff p_errors 2) `bracket` xdecref+  -- Should we convert to PyAsyncCancelled?+  ty_async_cancelled <- progIO [CU.exp| PyObject* { inline_py_AsyncCancelled() } |]+  when (p_type == ty_async_cancelled) $ do+    abort $ SomeException PyAsyncCancelled+  -- Should we convert to haskell exception?+  ty_hask_err <- progIO [CU.exp| PyObject* { inline_py_HaskellError() } |]+  when (p_type == ty_hask_err) $ do+    s_ptr <- progIO [CU.exp| void* { inline_py_HaskellError_get_stableptr($(PyObject* p_value)) } |]+    err   <- progIO $ deRefStablePtr $ castPtrToStablePtr s_ptr+    abort err+  -- Convert any other python exception   progPy $ do     s_type  <- pyobjectStrAsHask p_type     s_value <- pyobjectStrAsHask p_value     incref p_value     exc     <- newPyObject p_value     let bad_str = "__str__ call failed"-    pure $ PyException+    pure $ SomeException $ PyError $ PyException       { ty        = fromMaybe bad_str s_type       , str       = fromMaybe bad_str s_value       , exception = exc@@ -644,7 +921,7 @@ checkThrowPyError =   Py [CU.exp| PyObject* { PyErr_Occurred() } |] >>= \case     NULL -> pure ()-    _    -> throwM . PyError =<< convertPy2Haskell+    _    -> throwM =<< convertPy2Haskell  -- | Throw python error as haskell exception if it's raised. If it's --   not that internal error. Another exception will be raised@@ -652,7 +929,7 @@ mustThrowPyError =   Py [CU.exp| PyObject* { PyErr_Occurred() } |] >>= \case     NULL -> error $ "mustThrowPyError: no python exception raised."-    _    -> throwM . PyError =<< convertPy2Haskell+    _    -> throwM =<< convertPy2Haskell  -- | Calls mustThrowPyError if pointer is null or returns it unchanged throwOnNULL :: Ptr PyObject -> Py (Ptr PyObject)@@ -696,7 +973,7 @@  instance Namespace Main where   basicNamespaceDict _ =-    throwOnNULL =<< Py [CU.block| PyObject* {+    throwOnNULL =<< Py [C.block| PyObject* {       PyObject* main_module = PyImport_AddModule("__main__");       if( PyErr_Occurred() )           return NULL;@@ -743,7 +1020,7 @@  instance Namespace ModulePtr where   basicNamespaceDict (ModulePtr p) = do-    throwOnNULL =<< Py [CU.block| PyObject* {+    throwOnNULL =<< Py [C.block| PyObject* {       PyObject* dict = PyModule_GetDict($(PyObject* p));       Py_XINCREF(dict);       return dict;@@ -820,6 +1097,35 @@ {-# SPECIALIZE exec :: Main -> Main -> PyQuote -> Py () #-} {-# SPECIALIZE exec :: Main -> Temp -> PyQuote -> Py () #-} +-- | Evaluate code as if it's a function body and bound arguments are+--   parameters to that function. Just like function it could return+--   value and always creates new scope.+--+-- @since 0.3+evalPyFunction+  :: Namespace global+  => global     -- ^ Global variables+  -> PyQuoteFun -- ^ Source code+  -> Py PyObject+evalPyFunction globals (PyQuoteFun code binder) = runProgram $ do+  p_locals <- takeOwnership =<< progPy basicNewDict+  p_kwargs <- takeOwnership =<< progPy basicNewDict+  progPy $ do+    -- Create function in p_locals+    exec globals (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++getFunctionObject :: Ptr PyObject -> Py (Ptr PyObject)+getFunctionObject p_dict = do+  Py [CU.exp| PyObject* { PyDict_GetItemString($(PyObject *p_dict), "_inline_python_") } |]++ -- | Obtain pointer to code unsafeWithCode :: Code -> Program r (Ptr CChar) unsafeWithCode (Code bs) = Program $ ContT $ \fun ->@@ -831,7 +1137,7 @@ ----------------------------------------------------------------  debugPrintPy :: Ptr PyObject -> Py ()-debugPrintPy p = Py [CU.block| void {+debugPrintPy p = Py [C.block| void {   PyObject_Print($(PyObject *p), stdout, 0);   printf(" [REF=%li]\n", Py_REFCNT($(PyObject *p)) );   } |]
src/Python/Internal/EvalQQ.hs view
@@ -2,16 +2,12 @@ {-# LANGUAGE TemplateHaskell #-} -- | module Python.Internal.EvalQQ-  ( -- * Evaluators and QQ-    evaluatorPyf-    -- * Code generation-  , expQQ+  ( -- * Code generation+    expQQ   , Mode(..)   ) where  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)@@ -32,7 +28,6 @@ import Python.Internal.Types import Python.Internal.Program import Python.Internal.Eval-import Python.Internal.CAPI import Python.Inline.Literal  @@ -42,12 +37,16 @@ ----------------------------------------------------------------  -- | Python's variable name encoded using UTF-8. It exists in order to---   avoid working with @String@ at runtime.+--   avoid working with @String@ at runtime. Bytestring should be NULL+--   terminated manually newtype PyVarName = PyVarName BS.ByteString   deriving stock (Show, TH.Lift) +-- | PyVarName is being consumed using 'BS.unsafeUseAsCString' which+--   doesn't NULL terminate underlying buffer and python's C API+--   require it. We have to add terminator ourselves varName :: String -> PyVarName-varName = PyVarName . T.encodeUtf8 . T.pack+varName nm = PyVarName $ flip BS.snoc 0 $ T.encodeUtf8 (T.pack nm)  unsafeWithPyVarName :: PyVarName -> Program r (Ptr CChar) unsafeWithPyVarName (PyVarName bs)@@ -70,31 +69,6 @@   ------------------------------------------------------------------- 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-    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--getFunctionObject :: Ptr PyObject -> Py (Ptr PyObject)-getFunctionObject p_dict = do-  Py [CU.exp| PyObject* { PyDict_GetItemString($(PyObject *p_dict), "_inline_python_") } |]-------------------------------------------------------------------- -- TH generator ---------------------------------------------------------------- @@ -116,8 +90,7 @@       -> TH.Q TH.Exp expQQ mode qq_src = do   -- We need to preprocess before passing it to python.-  let src     = prepareSource       mode qq_src-      src_var = prepareForVarLookup mode src+  let src = prepareSource mode qq_src   antis  <- liftIO $ do     -- We've embedded script into library and we need to pass source     -- code of QQ to a script. It can contain whatever symbols so to@@ -133,7 +106,7 @@                 , "decode_and_print('" <>                   concat [ [ intToDigit $ fromIntegral (w `shiftR` 4)                            , intToDigit $ fromIntegral (w .&. 15) ]-                         | w <- BS.unpack $ T.encodeUtf8 $ T.pack src_var+                         | w <- BS.unpack $ T.encodeUtf8 $ T.pack src                          ]                   <> "')"                 ]@@ -145,9 +118,10 @@              ]       src_eval = prepareForEval mode antis src   ---  [| PyQuote ($(TH.lift $ codeFromString src_eval))-             (mconcat $(TH.listE args))-   |]+  case mode of+    Eval -> [| PyQuote    ($(TH.lift $ codeFromString src_eval)) (mconcat $(TH.listE args)) |]+    Exec -> [| PyQuote    ($(TH.lift $ codeFromString src_eval)) (mconcat $(TH.listE args)) |]+    Fun  -> [| PyQuoteFun ($(TH.lift $ codeFromString src_eval)) (mconcat $(TH.listE args)) |]   antiSuffix :: String@@ -167,12 +141,6 @@   Eval -> dropWhile isSpace   Exec -> unindent   Fun  -> unindent--prepareForVarLookup :: Mode -> String -> String-prepareForVarLookup = \case-  Eval -> id-  Exec -> id-  Fun  -> ("def __dummy__():\n"++) . indent  prepareForEval :: Mode -> [String] -> String -> String prepareForEval mode vars src = case mode of
src/Python/Internal/Program.hs view
@@ -6,6 +6,7 @@   , runProgram   , progPy   , progIO+  , progPyBracket   , progIOBracket     -- * Control flow   , abort@@ -38,7 +39,6 @@ import Foreign.Storable  import Language.C.Inline          qualified as C-import Language.C.Inline.Unsafe   qualified as CU  import Python.Internal.Types import Python.Internal.Util@@ -68,6 +68,9 @@ progIOBracket :: ((a -> IO r) -> IO r) -> Program r a progIOBracket = coerce +progPyBracket :: ((a -> Py r) -> Py r) -> Program r a+progPyBracket = coerce+ -- | Early exit from continuation monad. abort :: r -> Program r a abort r = Program $ ContT $ \_ -> pure r@@ -131,14 +134,14 @@ --   string. Returns Nothing if exception was raisede pyobjectStrAsHask :: Ptr PyObject -> Py (Maybe String) pyobjectStrAsHask p_obj = runProgram $ do-  p_str <- takeOwnership <=< abortOnNull Nothing $ Py [CU.block| PyObject* {+  p_str <- takeOwnership <=< abortOnNull Nothing $ Py [C.block| PyObject* {     PyObject *s = PyObject_Str($(PyObject *p_obj));     if( PyErr_Occurred() ) {         PyErr_Clear();     }     return s;     } |]-  c_str <- abortOnNull Nothing $ Py [CU.block| const char* {+  c_str <- abortOnNull Nothing $ Py [C.block| const char* {     const char* s = PyUnicode_AsUTF8($(PyObject *p_str));     if( PyErr_Occurred() ) {         PyErr_Clear();
src/Python/Internal/Types.hs view
@@ -15,10 +15,12 @@   , PyError(..)   , PyException(..)   , PyInternalError(..)+  , PyThreadId(..)   , Py(..)   , pyIO     -- ** Python code wrappers   , PyQuote(..)+  , PyQuoteFun(..)   , Code(..)   , codeFromText   , codeFromString@@ -34,13 +36,14 @@   , pattern TRUE   ) where -import Control.Monad.IO.Class import Control.Monad.Catch import Control.Monad.Primitive (PrimMonad(..),RealWorld)-import Control.Exception import Data.Coerce import Data.Int+import Data.Word import Data.ByteString             qualified as BS+import Data.ByteString.Lazy        qualified as BL+import Data.ByteString.Builder     qualified as BB import Data.Map.Strict             qualified as Map import Data.Text                   qualified as T import Data.Text.Encoding          qualified as T@@ -120,16 +123,17 @@ pyIO :: IO a -> Py a pyIO = Py --- | Removes exception masking-instance MonadIO Py where-  liftIO = Py . interruptible- instance PrimMonad Py where   type PrimState Py = RealWorld   primitive = Py . primitive   {-# INLINE primitive #-}  +-- | Identifier of python's thread as python understands them.+newtype PyThreadId = PyThreadId Word64+  deriving stock (Show,Eq,Ord)++ ---------------------------------------------------------------- -- Code wrappers ----------------------------------------------------------------@@ -146,6 +150,18 @@   }  +-- | Quasiquoted body of python function which could be used by+--   'Python.Inline.Eval.evalPyFunction'. Function body must be+--   wrapped in @_inline_python_@ function which quasiquoter+--   'Python.Inline.QQ.pyfun' does automatically.+--+--   @since 0.3@+data PyQuoteFun = PyQuoteFun+  { code   :: !Code+  , binder :: !DictBinder+  }++ -- | UTF-8 encoded python source code. -- --   @since 0.2@@@ -157,7 +173,13 @@ -- --   @since 0.2@ codeFromText :: T.Text -> Code-codeFromText = Code . T.encodeUtf8+-- NOTE: We consume code with BS.useAsCString and python C API expect+--       NULL-terminated string. We have to add terminator manually+codeFromText code+  = Code+  $ BL.toStrict+  $ BB.toLazyByteString+  $ T.encodeUtf8Builder code <> BB.word8 0  -- | Create properly encoded @Code@. This function doesn't check --   syntactic validity.
test/TST/Callbacks.hs view
@@ -1,6 +1,7 @@ -- | module TST.Callbacks (tests) where +import Control.Exception import Test.Tasty import Test.Tasty.HUnit import Python.Inline@@ -68,11 +69,21 @@   , testCase "Haskell exception in callback(arity=1)" $ runPy $ do        let foo :: Int -> IO Int            foo y = pure $ 10 `div` y-       throwsPy [py_| foo_hs(0) |]+       throwsException DivideByZero [py_| foo_hs(0) |]   , testCase "Haskell exception in callback(arity=2)" $ runPy $ do       let foo :: Int -> Int -> IO Int           foo x y = pure $ x `div` y-      throwsPy [py_| foo_hs(1, 0) |]+      throwsException DivideByZero [py_| foo_hs(1, 0) |]+  , testCase "Haskell exception has correct type" $ runPy $ do+       let foo :: Int -> IO Int+           foo y = pure $ 10 `div` y+       [py_|+         import inline_python+         try:+             foo_hs(0)+         except inline_python.HaskellError as e:+             assert repr(e) == "<inline_python.HaskellError: ArithException: divide by zero>", repr(e)+         |]     ----------------------------------------   , testCase "Call python in callback (arity=1)" $ runPy $ do       let foo :: Int -> IO Int
+ test/TST/Module.hs view
@@ -0,0 +1,44 @@+-- |+module TST.Module where++import Control.Exception+import Data.Typeable+import Test.Tasty+import Test.Tasty.HUnit+import Python.Inline+import Python.Inline.QQ+import Python.Inline.Async++tests :: TestTree+tests = testGroup "Builtin module"+  [ testCase "Module exists" $ runPy [py_| import inline_python |]+  , testCase "Exceptions" $ runPy [py_|+      import inline_python+      assert issubclass(inline_python.AsyncCancelled, BaseException)+      assert not issubclass(inline_python.AsyncCancelled, Exception)++      assert issubclass(inline_python.HaskellError, Exception),     "HaskellError is Exception"+      assert issubclass(inline_python.HaskellError, BaseException), "HaskellError is BaseException"+      |]+    -- We want to check that inline_python types are stable under+    -- reload using importlib.+  , testCase "importlib.reload stable" $ runPy [py_|+      import inline_python+      import importlib+      ty  = inline_python.AsyncCancelled+      err = ty()+      importlib.reload(inline_python)+      assert ty is inline_python.AsyncCancelled+      assert isinstance(err, inline_python.AsyncCancelled)+      |]+  , testCase "AsyncCancelled is converted to PyAsyncCancelled" $ do+    r :: Either SomeException () <- try $ runPy [py_|+      import inline_python+      raise inline_python.AsyncCancelled()+      |]+    case r of+      Right () -> error "No exception"+      Left  (SomeException e)+        | Just PyAsyncCancelled <- cast e -> pure ()+        | otherwise                       -> throwIO e+  ]
test/TST/Run.hs view
@@ -3,26 +3,42 @@ module TST.Run(tests) where  import Control.Concurrent+import Control.Concurrent.STM import Control.Exception import Control.Monad import Control.Monad.IO.Class import Data.Map.Strict        qualified as Map+import Data.Text              qualified as T import Test.Tasty import Test.Tasty.HUnit import Python.Inline import Python.Inline.QQ import Python.Inline.Eval+import Python.Inline.Async import TST.Util  tests :: TestTree tests = testGroup "Run python"   [ testCase "Empty QQ" $ runPy [py_| |]   , testCase "Second init is noop" $ initializePython-  , testCase "Nested runPy" $ runPy $ liftIO $ runPy $ pure ()+  , testRunPy+  , testScope+  , testEvalExec+  , testMonadic+  , testAsync+  ]++-- Test that python evaluation works in general and its interaction+-- with exceptions+testRunPy :: TestTree+testRunPy = testGroup "Running python"+  [ testCase "Nested runPy" $ runPy $ liftIO $ runPy $ pure ()   , testCase "Nested runPyInMain" $ runPyInMain $ liftIO $ runPyInMain $ pure ()   , testCase "runPyInMain" $ runPyInMain $ [py_|       import threading-      assert threading.main_thread() == threading.current_thread()+      tid_main = threading.main_thread()+      tid_our  = threading.current_thread()+      assert tid_main == tid_our, f"TID[main]={tid_main}, TID[our]={tid_our}"       |]   , testCase "Python exceptions are converted (py)"   $ runPy      $ throwsPy    [py_| 1 / 0 |]   , testCase "Python exceptions are converted (std)"  $ throwsPyIO $ runPy       [py_| 1 / 0 |]@@ -31,18 +47,21 @@       throwsPyIO $ runPyInMain [py_| 1 / 0 |]       runPyInMain [py_| assert True |]     -- Here we test that exceptions are really passed to python's thread without running python-  , testCase "Exception in runPyInMain works" $ do+  , testCase "Exception in runPyInMain works hask" $ do       lock <- newEmptyMVar       tid  <- myThreadId-      _    <- forkIO $ takeMVar lock >> throwTo tid Stop+      _    <- forkIO $ takeMVar lock >> threadDelay 1000 >> throwTo tid Stop       handle (\Stop -> pure ())         $ runPyInMain         $ do liftIO $ putMVar lock ()              liftIO $ threadDelay 10_000_000              error "Should be interrupted"-      runPyInMain $ pure ()-  ---  , testCase "Scope pymain->any" $ runPy $ do+  ]++-- Test that different quasiquotes correctly handle scoping+testScope :: TestTree+testScope = testGroup "Python scope"+  [ testCase "Scope pymain->any" $ runPy $ do       [pymain|              x = 12              x@@ -133,7 +152,11 @@         except NameError:             pass         |]-  , testCase "pyf works" $ do+  ]++testEvalExec :: TestTree+testEvalExec = testGroup "eval/exec"+  [ testCase "pyf works" $ do       let x = 12 :: Int       eq (Just (482412::Int)) [pyf|          xs = [i*x_hs for i in [1, 200, 40000]]@@ -164,8 +187,100 @@         assert m_hs.a == 12         assert m_hs.b == 'asd'         |]+  , testCase "evalPyFunction" $ runPy $ do+      let n = 2 :: Int+      x <- fromPy =<< evalPyFunction Temp [pyfun| return 100 * n_hs |]+      liftIO $ Just (200::Int) @=? x+  , testCase "createModule" $ runPy $ do+      m <- createModule (T.pack "foo") [pycode||]+      [py_|+        import types+        assert isinstance(m_hs, types.ModuleType)+        |]   ] +testAsync :: TestTree+testAsync = testGroup "async" $ guardThreaded+  [ -- We can run async computation at all+    testCase "runPyAsync" $ do+      runPy [pymain| dct = {} |]+      a <- runPyAsync $ [py_| dct[1] = 100 |]+      _ <- atomically $ waitPy a+      n <- runPy $ fromPy =<< [pye| dct[1] |]+      assertEqual "x" (Just (100::Int)) n+      runPy [pymain| del dct |]+  , -- Cancellation of python code+    testCase "cancelPy [python]" $ do+      a <- runPyAsync $ forever $ [py_|+        import time+        while True:+            time.sleep(1e-3)+        |]+      d <- registerDelay 100_000+      threadDelay 100 -- Wait to make sure execution actually started+      cancelPy a+      _ <- atomically $ waitPyCatch a `orElse` do readTVar d >>= \case+                                                    True  -> error "Timeout"+                                                    False -> retry+      return ()+  , -- Cancellation of haskell code+    testCase "cancelPy [haskell]" $ do+      a <- runPyAsync $ do+        liftIO $ forever $ threadDelay 1_000_000+      d <- registerDelay 100_000+      threadDelay 100+      cancelPy a+      _ <- atomically $ waitPyCatch a `orElse` do readTVar d >>= \case+                                                    True  -> error "Timeout"+                                                    False -> retry+      return ()+  , -- Cancellation of haskell code+    testCase "cancelPy [callback]" $ do+      a <- runPyAsync $ do+        let loop = forever $ threadDelay 1_000_000 :: IO ()+        forever [py_| loop_hs() |]+      d <- registerDelay 100_000+      threadDelay 100+      _ <- forkIO $ cancelPy a+      _ <- atomically $ waitPyCatch a `orElse` do readTVar d >>= \case+                                                    True  -> error "Timeout"+                                                    False -> retry+      return ()+  , testCase "Exception in runPyInMain works py" $ do+      lock <- newEmptyMVar+      tid  <- myThreadId+      _    <- forkIO $ takeMVar lock >> threadDelay 1000 >> throwTo tid Stop+      handle (\Stop -> pure ())+        $ runPyInMain+        $ do liftIO $ putMVar lock ()+             [py_|+               import time+               while True:+                   time.sleep(1e-3)+               |]+             error "Should be interrupted"+  ]+++-- Here we only test that quasiquotes produce correct code+testMonadic :: TestTree+testMonadic = testGroup "Monadic"+  [ testCase "pymain" $ runPy [pymain| assert True |]+  , testCase "py_"    $ runPy [py_|    assert True |]+  , testCase "pye" $ runPy $ do+      n <- fromPy =<< [pye| 42 |]+      liftIO $ Just (42::Int) @=? n+  , testCase "pyf" $ runPy $ do+      n <- fromPy =<< [pyf| return 42 |]+      liftIO $ Just (42::Int) @=? n+  ]++ data Stop = Stop   deriving stock    Show   deriving anyclass Exception++guardThreaded :: [TestTree] -> [TestTree]+guardThreaded ts+  | rtsSupportsBoundThreads = ts+  | otherwise               = []
test/TST/Util.hs view
@@ -3,6 +3,7 @@  import Control.Monad.IO.Class import Control.Monad.Catch+import Data.Typeable import Test.Tasty.HUnit  import Python.Inline@@ -10,6 +11,12 @@ throwsPy :: Py () -> Py () throwsPy io = (io >> liftIO (assertFailure "Evaluation should raise python exception"))   `catch` (\(_::PyError) -> pure ())++throwsException :: (Exception e, Eq e) => e -> Py () -> Py ()+throwsException e0 io = (io >> liftIO (assertFailure "Evaluation should raise python exception"))+  `catch` (\(SomeException e) -> case cast e of Just e' | e' == e0 -> pure ()+                                                _                  -> throwM e+          )  throwsPyIO :: IO () -> IO () throwsPyIO io = (io >> assertFailure "Evaluation should raise python exception")
test/exe/main.hs view
@@ -7,6 +7,7 @@ import TST.ToPy import TST.Callbacks import TST.Roundtrip+import TST.Module import Python.Inline  main :: IO ()@@ -16,4 +17,5 @@   , TST.ToPy.tests   , TST.Roundtrip.tests   , TST.Callbacks.tests+  , TST.Module.tests   ]