#+PROPERTY: HEADER-ARGS+ :eval no-export :exports both * Intro An org document with code blocks to help test the proposed patchs. See [[*On top of ob-shell][On top of ob-shell]] for asynchronous shell scripts using ob-shell. See [[*On top of ob-python][On top of ob-python]] for asynchronous python scripts using ob-python. You need to load: #+begin_src elisp :results silent (setq-local org-confirm-babel-evaluate nil) (load-file "my-async-tests.el") #+end_src Emacs and org versions: #+begin_src elisp (mapcar (lambda (sb) (list sb (symbol-value sb))) '(emacs-version org-version)) #+end_src #+RESULTS: | emacs-version | 30.0.50 | | org-version | 9.7-pre | Note that we've disabled eval on export: export doesn't know it needs to wait for asynchronous results. * POSIX shells ** A simple bash example :PROPERTIES: :header-args:bash: :execute-with my-shell-babel :nasync yes :END: The package `my-async-tests.el' contains the function `my-shell-babel-schedule' to evaluate shell script asynchronously. The header-args properties above request asynchronous execution for bash (:nasync yes), and, tells ob-core to use the prefix `my-shell-babel' when looking for functions to evaluate a source block. Thus, org will delegate execution to `my-shell-babel-schedule'. We don't have `my-shell-babel-execute', so, in this case, :nasync must be yes. Examples taken from the org mailing list and from worg. A simple execution: #+begin_src bash date #+end_src #+RESULTS: : Wed Feb 21 15:56:23 CET 2024 A tricky computation takes some time: #+begin_src bash sleep 1; date #+end_src #+RESULTS: : Wed Feb 21 15:56:25 CET 2024 An example of a failure: #+begin_src bash sleepdd 1; false #+end_src #+RESULTS: ** On top of ob-shell :PROPERTIES: :header-args:bash: :execute-with my-org-babel :nasync yes :header-args:bash+: :session sh-async :END: #+begin_src bash sleep 1; date #+end_src #+RESULTS: : Wed Feb 21 15:56:42 CET 2024 #+begin_src bash :results output :session *test* :nasync yes cd /tmp echo "hello world" #+end_src #+RESULTS: : hello world #+begin_src bash :results output # comment # comment #+end_src #+RESULTS: #+begin_src bash :results output # print message echo \"hello world\" #+end_src #+RESULTS: : "hello world" #+begin_src bash :results output echo "hello" echo "world" #+end_src #+RESULTS: : hello : world #+begin_src bash :results output echo PID: "$$" #+end_src #+RESULTS: : PID: 22212 #+begin_src bash :results output echo PID: "$$" #+end_src #+RESULTS: : PID: 22212 #+begin_src bash :results output :session shared echo PID: "$$" X=5 #+end_src #+RESULTS: : PID: 22218 #+begin_src bash :results output :session shared echo PID: "$$" echo X was set to "$X" #+end_src #+RESULTS: : PID: 22218 : X was set to 5 #+begin_src bash :nasync yes :results value scalar echo "Execute session blocks in the background" sleep 3 echo "Using the :async header" #+end_src #+RESULTS: : Execute session blocks in the background : Using the :async header : 0 #+name: their-os Linux #+begin_src bash :results output :shebang #!/usr/bin/env bash :stdin their-os :cmdline RMS :tangle ask_for_os.sh # call as ./ask_for_os.sh NAME, where NAME is who to ask if [ -z "$1" ]; then asked="$USER" else asked="$1" fi echo Hi, "$asked"! What operating system are you using? read my_os if [ "$asked" = "RMS" ]; then echo You\'re using GNU/"$my_os"! elif [ "$asked" = "Linus" ]; then echo You\'re using "$my_os"! else echo You\'re using `uname -o`! fi #+end_src #+RESULTS: : Hi, RMS! What operating system are you using? : You're using GNU/Linux! #+begin_src bash declare -a array m=4 n=3 for ((i=0; i foo_file echo "Created foo_file" else echo "foo_file already exists!" fi #+end_src #+RESULTS: : foo_file already exists! #+begin_src bash :results output :dir /ssh:phone: :session *remote* if [ ! -e "foo_file" ]; then echo "foo" > foo_file echo "Created foo_file" else echo "foo_file already exists!" fi #+end_src #+RESULTS: : Created foo_file #+RESULTS: #+begin_src bash :results none :session *my-session* X=1 #+end_src #+RESULTS: #+begin_src bash :results output :session *my-session* echo X was set to "$X" #+end_src #+RESULTS: : X was set to 1 #+begin_src bash :results output :session *another-session* echo X was set to "$X" #+end_src #+RESULTS: : X was set to #+RESULTS: #+begin_src bash :results output echo "Hello, world!" sleep 3 echo "Good-bye, cruel World..." #+end_src #+RESULTS: : Hello, world! : Good-bye, cruel World... #+begin_src bash :var by_two=0 x=3 :session none if [ "$by_two" = "0" ]; then echo $(($x * 2)) else echo $(($x * 3)) fi #+end_src #+RESULTS: : 6 #+begin_src bash :results output :var arr='("apple" "banana" "cherry") echo The first element is... echo \"${arr[1]}\" #+end_src #+RESULTS: : The first element is... : "banana" *** TODO Doesn't work yet: asynchronous that depends from other blocks #+name: multiply_by_2 #+begin_src bash :var data="" :results output echo $(($data * 2)) #+end_src #+RESULTS: multiply_by_2 : bash: * 2: syntax error: operand expected (error token is "* 2") #+begin_src bash :post multiply_by_2(data=*this*) echo 3 #+end_src #+results: * On top of ob-python :PROPERTIES: :header-args:python: :execute-with my-org-babel :nasync yes :header-args:python+: :session py-async :END: Used =header-args= properties: - =:execute-with my-org-babel=: look for functions with the prefix `my-org-babel' to execute blocks (for the asynchronous case use `my-org-babel-schedule', and, for the synchronous case `my-org-babel-execute'). These functions are defined in [[file:my-async-tests.el]]. - =:nasync yes=: by default, execute asynchronously (use `my-org-babel-schedule'). - =:session py-async= by default, use a session named "py-async". ** basic examples *** async with a session A very simple test: #+begin_src python 2+3 #+end_src #+RESULTS: : 5 Let's import the module time in our session. #+begin_src python :results silent import time #+end_src #+RESULTS: (Yes, =:results silent= needs some work.) A table that requires some time to compute: #+begin_src python start = time.time() time.sleep(1) end = time.time() ["%.1fs" % t for t in [start, end, end-start]] #+end_src #+RESULTS: | 1708515666.8s | 1708515667.8s | 1.0s | An error (click on the error , , to see the details): #+begin_src python 2/0 #+end_src #+RESULTS: *** async with no session :PROPERTIES: :header-args:python+: :session none :END: A very simple test: #+begin_src python 2+3 #+end_src #+RESULTS: : 5 Let's import the module time in our session. #+begin_src python :results silent import time #+end_src #+RESULTS: (Yes, =:results silent= needs some work.) A table that requires some time to compute: #+begin_src python start = time.time() time.sleep(1) end = time.time() ["%.1fs" % t for t in [start, end, end-start]] #+end_src #+RESULTS: | 1708083470.9s | 1708083471.9s | 1.0s | Yes, it failed, as expected. "import time" was done in its own temporary session. The old result is preserved; the error is display as an overlay. Click on it to get more info about the error. Let's fix it, adding the import line: #+begin_src python import time start = time.time() time.sleep(1) end = time.time() ["%.1fs" % t for t in [start, end, end-start]] #+end_src #+RESULTS: | 1708515915.0s | 1708515916.0s | 1.0s | An error (click on the error , , to see the details): #+begin_src python 2/0 #+end_src #+RESULTS: *** sync with a session :PROPERTIES: :header-args:python+: :session py-sync-session :nasync no :END: A very simple test: #+begin_src python 2+3 #+end_src #+RESULTS: : 5 Let's import the module time in our session. #+begin_src python :results silent import time #+end_src (Yes, =:results silent= needs some work.) A table that requires some time to compute: #+begin_src python start = time.time() time.sleep(1) end = time.time() ["%.1fs" % t for t in [start, end, end-start]] #+end_src #+RESULTS: | 1708102997.5s | 1708102998.5s | 1.0s | An error (click on the error , , to see the details): #+begin_src python 2/0 #+end_src #+RESULTS: *** sync with no session :PROPERTIES: :header-args:python+: :session none :nasync no :END: A very simple test: #+begin_src python 2+3 #+end_src #+RESULTS: : 5 Let's import the module time in our session. #+begin_src python :results silent import time #+end_src (Yes, =:results silent= needs some work.) A table that requires some time to compute: #+begin_src python start = time.time() time.sleep(1) end = time.time() ["%.1fs" % t for t in [start, end, end-start]] #+end_src #+RESULTS: | 1708083470.9s | 1708083471.9s | 1.0s | Yes, that fails (no session), displaying the details in a popup. Let's fix it: #+begin_src python import time start = time.time() time.sleep(1) end = time.time() ["%.1fs" % t for t in [start, end, end-start]] #+end_src #+RESULTS: | 1708103039.0s | 1708103040.0s | 1.0s | An error (click on the error , , to see the details): #+begin_src python 2/0 #+end_src #+RESULTS: ** worg examples Let's import matplotlib in our session. #+begin_src python import matplotlib import matplotlib.pyplot as plt #+end_src #+RESULTS: : None A figure in a PDF, asynchronous case. #+begin_src python :results file link fig=plt.figure(figsize=(3,2)) plt.plot([1,3,2]) fig.tight_layout() fname = 'myfig-async.pdf' plt.savefig(fname) fname # return this to org-mode #+end_src #+RESULTS: [[file:myfig-async.pdf]] A figure in a PDF, synchronous case. #+begin_src python :results file link :nasync no fig=plt.figure(figsize=(3,2)) plt.plot([1,3,2]) fig.tight_layout() fname = 'myfig-sync.pdf' plt.savefig(fname) fname # return this to org-mode #+end_src #+RESULTS: [[file:myfig-sync.pdf]] A PNG figure, asynchronous case. #+begin_src python :results graphics file output :file boxplot.png fig=plt.figure(figsize=(3,2)) plt.plot([1,3,2]) fig.tight_layout() fig #+end_src #+RESULTS: [[file:boxplot.png]] Same, but using the =:return= keyword. #+begin_src python :return "plt.gcf()" :results graphics file output :file boxplot.png fig=plt.figure(figsize=(3,2)) plt.plot([1,3,2]) fig.tight_layout() #+end_src #+RESULTS: [[file:boxplot.png]] Same, asynchronous but without a session this time. #+begin_src python :return "plt.gcf()" :results graphics file output :file boxplot-no-sess-a-y.png :session none import matplotlib import matplotlib.pyplot as plt fig=plt.figure(figsize=(3,2)) plt.plot([1,3,2]) fig.tight_layout() #+end_src #+RESULTS: [[file:boxplot-no-sess-a-y.png]] Lists are table, #+begin_src python [1,2,3] #+end_src #+RESULTS: | 1 | 2 | 3 | unless requested otherwise. #+begin_src python :results verbatim [1,2,3] #+end_src #+RESULTS: : [1, 2, 3] Dictionaries are tables too. #+begin_src python :results table {"a": 1, "b": 2} #+end_src #+RESULTS: | a | 1 | | b | 2 | Let's try the example with Panda. #+begin_src python :results none import pandas as pd import numpy as np #+end_src #+RESULTS: : None #+begin_src python :results table pd.DataFrame(np.array([[1,2,3],[4,5,6]]), columns=['a','b','c']) #+end_src #+RESULTS: | | a | b | c | |---+---+---+---| | 0 | 1 | 2 | 3 | | 1 | 4 | 5 | 6 | And the synchronous case? #+begin_src python :results table :nasync no pd.DataFrame(np.array([[1,2,3],[4,5,6]]), columns=['a','b','c']) #+end_src #+RESULTS: | | a | b | c | |---+---+---+---| | 0 | 1 | 2 | 3 | | 1 | 4 | 5 | 6 | Without session ? #+begin_src python :results table :session none pd.DataFrame(np.array([[1,2,3],[4,5,6]]), columns=['a','b','c']) #+end_src #+RESULTS: | | a | b | c | |---+---+---+---| | 0 | 1 | 2 | 3 | | 1 | 4 | 5 | 6 | Right, we need to import the libraries (no session). #+begin_src python :results table :session none import pandas as pd import numpy as np pd.DataFrame(np.array([[1,2,3],[4,5,6]]), columns=['a','b','c']) #+end_src #+RESULTS: | | a | b | c | |---+---+---+---| | 0 | 1 | 2 | 3 | | 1 | 4 | 5 | 6 | ** inline examples A simple asynchronous inline src_python{3*2} {{{results(=6=)}}}. An other one containing a mistake src_python{2/0} {{{results(=6=)}}} (click on the error to see the details). Some very slow inline asynchronous computations that all run in the same session. You need to execute the 3 of them at once. Here is the first one src_python[:return "\"OK1\""]{import time; time.sleep(5)} {{{results(=OK1=)}}} and a second one src_python[:return "\"OK1 bis\""]{import time; time.sleep(5)} {{{results(=OK1 bis=)}}} and the third one src_python[:return "\"OK2\""]{import time; time.sleep(5)} {{{results(=OK2=)}}}. Yes, the previous paragraph is unreadable; it's on purpose, to check that ob-core can figure it out. Let's repeat, in a more readable way, and making the last one synchronous. Some very slow inline computations that all run in the same session. Here is the first asynchronous one src_python[:return"\"OK1\""]{import time; time.sleep(5)} {{{results(=None=)}}} and a second one, asynchronous too: src_python[:return "\"OK1 bis\""]{import time; time.sleep(5)} {{{results(=OK1 bis=)}}} and finally, a third one, synchronous this one: src_python[:nasync no :return "\"OK2\""]{import time; time.sleep(5)} {{{results(=OK2=)}}}. Note that, once the user executes the last synchronous block, the user is blocked until the synchronous execution can start (i.e. all previous asynchronous executions are done) and until it's done. The display is updated though, to see the asynchronous progress.