Skip to content

Commit 4afaad4

Browse files
authored
Merge pull request #54 from Shimuuar/documentation
Update haddocks and changelog
2 parents 61b4509 + c4b7661 commit 4afaad4

5 files changed

Lines changed: 89 additions & 51 deletions

File tree

‎ChangeLog.md‎

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
0.3.0.0 [XXXX.XX.XX]
22
--------------------
3-
* Support for builds with `python3-config`
4-
* Support for async
5-
* `inline_python` module is now available.
6-
* Now haskell exception from haskell callback in converted to
7-
`inline_python.HaskellError` and is rethrown if it's not catched by python.
8-
* Memory leak is fixed. Python exception object were never freed when exception
9-
propagated to haskell side.
3+
* Support for asynchronous execution added in module `Python.Inline.Async`.
4+
It adds API copied from `async` and interruptible python computations.
5+
* `runPyInMain` could be reliably interrupted by asynchronous exceptions.
6+
* Package now uses `Custom` build type. It now supports configuring python using
7+
`python3-config` instead of `pkg-config` when `-fpython3-config` manual cabal
8+
flag is set. Default behavior is unchanged.
9+
* Python module `inline_python` is now available. It contains exception types
10+
used by library: `AsyncCancelled` and `HaskellError` which wraps haskell
11+
exception from callback.
12+
* Haskell exception raised in haskell callback will be rethrown if not caught by
13+
python instead of being converted to `PyError`.
14+
* Memory leak in exception handling is fixed. Python exception object were never
15+
freed when exception propagated to haskell side.
1016

1117
0.2.1.0 [2026.01.13]
1218
----------------

‎src/Python/Inline.hs‎

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -177,11 +177,3 @@ import Python.Internal.Eval
177177
-- redone and python does not give much guarantee about what is happening here.
178178
-- Use it with caution. We recommend using @importlib.reload@ only during
179179
-- development and not in production.
180-
--
181-
-- 5. __Asynchronous exceptions__
182-
--
183-
-- The code run by 'runPy' is not interruptible by Haskell asynchronous
184-
-- exceptions and may block indefinitely. If your code call any Haskell
185-
-- function as callback, they won't receive asynchronous exception either. See
186-
-- https://github.com/Shimuuar/inline-python/issues/48 for details and
187-
-- workarounds.

‎src/Python/Inline/Async.hs‎

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
-- |
2-
-- Asynchronous computation using python. Normally library tries to
3-
-- execute python code in the same thread. Moreover it use global lock
4-
-- in addition to GIL in order to avoid blocking capability on GIL.
5-
-- This module provide API for working with concurrent python.
6-
-- Its API is heavily modelled after @async@ package.
2+
-- Asynchronous computation using python. Its API is modelled after
3+
-- @async@ package. It evaluates python on separate OS thread so it's
4+
-- more heavyweight than 'Python.Inline.runPy'. But it's possible to
5+
-- properly interrupt running computation with 'cancelPy' or to use
6+
-- 'withPyAsync' to ensure that async computation properly terminated.
77
--
8-
-- Note it's very experimental and not well tested. Also mixing
9-
-- concurrency primitives from two languages makes difficult task of
10-
-- concurrent programming even more complicated.
8+
-- Since arbitrary IO is available either in @Py@ via @liftIO@ or in
9+
-- haskell callbacks from python code. It's possible to use haskell
10+
-- concurrency primitives to communicate with python thread.
1111
module Python.Inline.Async
1212
( PyAsync
1313
, PyAsyncCancelled(..)
@@ -20,4 +20,3 @@ module Python.Inline.Async
2020
) where
2121

2222
import Python.Internal.Eval
23-

‎src/Python/Inline/QQ.hs‎

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,13 @@
3030
-- > do_that()
3131
-- > |]
3232
--
33-
-- If control over python's global and local variables is
34-
-- required. APIs from "Python.Inline.Eval" should be used instead.
33+
--
34+
-- == Variable scope
35+
--
36+
-- Python has two copes: global and local variables. Both are simply
37+
-- @dict[str,Any]@. Quasiquoters use different dictionaries for
38+
-- globals and locals. If tighter control over variables scope is
39+
-- required APIs from "Python.Inline.Eval" should be used instead.
3540
module Python.Inline.QQ
3641
( pymain
3742
, py_
@@ -46,9 +51,10 @@ import Python.Internal.EvalQQ
4651
import Python.Internal.Eval
4752

4853

49-
-- | Evaluate sequence of python statements. It works in the same way
50-
-- as python's @exec@. All module imports and all variables defined
51-
-- in this quasiquote will be visible to later quotes.
54+
-- | Evaluate sequence of python statements. It uses python's @exec@.
55+
-- Both global and local state for this quasiquoter are variables of
56+
-- @\__main__@ module. Any variables including imported modules will
57+
-- remain visible to later quasiquotes.
5258
--
5359
-- It creates value of type @Py ()@
5460
pymain :: QuasiQuoter
@@ -59,9 +65,11 @@ pymain = QuasiQuoter
5965
, quoteDec = error "quoteDec"
6066
}
6167

62-
-- | Evaluate sequence of python statements. All module imports and
63-
-- all variables defined in this quasiquote will be discarded and
64-
-- won't be visible in later quotes.
68+
-- | Evaluate sequence of python statements. Global variables for this
69+
-- quasiquoter are one defined in @\__main__@ module and locals use
70+
-- newly allocated dictionary. It will be discarded after execution
71+
-- so variables defined in this quasiquote are visible only inside
72+
-- of it.
6573
--
6674
-- It creates value of type @Py ()@
6775
py_ :: QuasiQuoter
@@ -73,7 +81,8 @@ py_ = QuasiQuoter
7381
}
7482

7583
-- | Evaluate single python expression. It only accepts single
76-
-- expressions same as python's @eval@.
84+
-- expressions same as python's @eval@. Its globals are variables in
85+
-- @\__main__@ module and locals are new dictionary same as in @py_@.
7786
--
7887
-- This quote creates object of type @Py PyObject@
7988
pye :: QuasiQuoter
@@ -85,9 +94,12 @@ pye = QuasiQuoter
8594
}
8695

8796
-- | Another quasiquoter which works around that sequence of python
88-
-- statements doesn't have any value associated with it. Content of
97+
-- statements doesn't have any value associated with it. Content of
8998
-- quasiquote is function body. So to get value out of it one must
90-
-- call return
99+
-- call return. Its globals are variables in @\__main__@ module and
100+
-- locals are new dictionary same as in @py_@.
101+
--
102+
-- This quote creates object of type @Py PyObject@
91103
pyf :: QuasiQuoter
92104
pyf = QuasiQuoter
93105
{ quoteExp = \txt -> [| evaluatorPyf $(expQQ Fun txt) |]

‎src/Python/Internal/Eval.hs‎

Lines changed: 45 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -524,9 +524,15 @@ data EvalReq
524524
-- ^ Dummy request. Do nothing
525525

526526

527-
-- | Execute python action. It will take and hold global lock while
528-
-- code is executed. Python exceptions raised during execution are
529-
-- converted to haskell exception 'PyError'.
527+
-- | Execute python action. This is simplest executor with lowest
528+
-- overhead but it comes with several caveats. When thread executes
529+
-- python code it could not be interrupted since it's in foreign
530+
-- call. Use 'runPyAsync' if you need ability to interrupt. Also
531+
-- python uses GIL so only one thread can evaluate python code at
532+
-- time.
533+
--
534+
-- Python exceptions raised during execution are converted to
535+
-- haskell exception 'PyError'.
530536
runPy :: Py a -> IO a
531537
-- See NOTE: [Python and threading]
532538
runPy py
@@ -538,10 +544,15 @@ runPy py
538544
go = ensurePyLock $ mask_ $ unsafeRunPy (ensureGIL py)
539545

540546

541-
-- | Same as 'runPy' but will make sure that code is run in python's
542-
-- main thread. It's thread in which python's interpreter was
543-
-- initialized. Some python's libraries may need that. It has higher
544-
-- call overhead compared to 'runPy'.
547+
-- | This function executes python code on python's main thread. It's
548+
-- OS thread in which interpreter was initialized and it has some
549+
-- special status in python. Some libraries could only work when
550+
-- called from main thread. It has higher call overhead compared to
551+
-- 'runPy' and only one haskell thread could be executing something
552+
-- on main thread at time.
553+
--
554+
-- When executing on threaded runtime this function could be
555+
-- interrupted by asynchronous exceptions.
545556
runPyInMain :: Py a -> IO a
546557
-- See NOTE: [Python and threading, Main thread]
547558
runPyInMain py
@@ -625,15 +636,22 @@ unsafeRunPy (Py io) = io
625636
-- thread is dead already.
626637

627638

628-
-- | Exception thrown to a thread doing async python computation.
639+
-- | Exception thrown to a thread doing async python computation. On
640+
-- python side it corresponds to
641+
-- @inline_python.AsyncCancelled@. Latter is automatically converted
642+
-- to @PyAsyncCancelled@.
643+
--
644+
-- @since 0.3
629645
data PyAsyncCancelled = PyAsyncCancelled
630646
deriving (Show, Eq)
631647

632648
instance Exception PyAsyncCancelled
633649

634650
-- | Handle to asynchronous python computation spawned by
635651
-- 'runPyAsync'. It's performed on separate OS thread. Use
636-
-- 'wait'\/'waitCatch' to obtain computation result.
652+
-- 'waitPy'\/'waitPyCatch' to obtain computation result.
653+
--
654+
-- @since 0.3
637655
data PyAsync a = PyAsync
638656
{ asyncTID :: !ThreadId -- Thread ID
639657
, asyncTidStack :: !(TVar [ThreadId]) -- Stack of callback thread ID
@@ -644,15 +662,21 @@ data PyAsync a = PyAsync
644662

645663
-- | Wait for result of asynchronous computation. If it threw an
646664
-- exception it will be rethrown by @wait@.
665+
--
666+
-- @since 0.3
647667
waitPy :: PyAsync a -> STM a
648668
waitPy a = either throwSTM pure =<< a.asyncWait
649669

650670
-- | Wait for result of asynchronous computation. Exception thrown by
651671
-- it will be returned as @Left@.
672+
--
673+
-- @since 0.3
652674
waitPyCatch :: PyAsync a -> STM (Either SomeException a)
653675
waitPyCatch = (.asyncWait)
654676

655-
-- | Create new OS thread and execute python code on it.
677+
-- | Execute python computation on dedicated OS thread.
678+
--
679+
-- @since 0.3
656680
runPyAsync :: Py a -> IO (PyAsync a)
657681
runPyAsync py = do
658682
ensureInit
@@ -700,15 +724,16 @@ withAsyncInitTLS stack = bracket ini fini . const
700724

701725

702726

703-
-- | Cancel execution of asynchronous computation. Most likely thread
704-
-- will be executing some python so first it attempts to raise async
705-
-- exception in python code. Then it throws 'PyAsyncCancelled' in case
706-
-- it executes haskell code. This means thread could be terminate
707-
-- either with 'PyError' or 'PyAsyncCancelled'.
727+
-- | Cancel execution of asynchronous computation. It throws
728+
-- 'PyAsyncCancelled' to haskell threads including any haskell
729+
-- callbacks from python. Python will interrupted by asynchronously
730+
-- raising @inline_python.AsyncCancelled@.
708731
--
709732
-- Note that python code generally is not written under assumption
710733
-- that it could be smitten with exception at an absolutely any
711-
-- moment.
734+
-- moment. It could cause problems.
735+
--
736+
-- @since 0.3
712737
cancelPy :: PyAsync a -> IO ()
713738
cancelPy PyAsync{asyncTID=tid, asyncTidStack, asyncPyTID, asyncAlive} = do
714739
-- See NOTE: [Py Async], [Interrupting python]
@@ -744,11 +769,15 @@ cancelPy PyAsync{asyncTID=tid, asyncTidStack, asyncPyTID, asyncAlive} = do
744769
killThread tid_kill_cb
745770

746771
-- | Variant of 'cancel' which isn't interruptible.
772+
--
773+
-- @since 0.3
747774
uninterruptibleCancelPy :: PyAsync a -> IO ()
748775
uninterruptibleCancelPy = uninterruptibleMask_ . cancelPy
749776

750777
-- | Create new OS thread and execute python code on it. Will use
751778
-- 'uninterruptibleCancel' after callback finishes execution.
779+
--
780+
-- @since 0.3
752781
withPyAsync :: Py a -> (PyAsync a -> IO b) -> IO b
753782
withPyAsync py = bracket (runPyAsync py) uninterruptibleCancelPy
754783

0 commit comments

Comments
 (0)