dotemacs

My Emacs configuration
git clone git://git.entf.net/dotemacs
Log | Files | Refs | LICENSE

ob-python.el (17320B)


      1 ;;; ob-python.el --- Babel Functions for Python      -*- lexical-binding: t; -*-
      2 
      3 ;; Copyright (C) 2009-2023 Free Software Foundation, Inc.
      4 
      5 ;; Authors: Eric Schulte
      6 ;;	 Dan Davison
      7 ;; Maintainer: Jack Kamm <jackkamm@gmail.com>
      8 ;; Keywords: literate programming, reproducible research
      9 ;; URL: https://orgmode.org
     10 
     11 ;; This file is part of GNU Emacs.
     12 
     13 ;; GNU Emacs is free software: you can redistribute it and/or modify
     14 ;; it under the terms of the GNU General Public License as published by
     15 ;; the Free Software Foundation, either version 3 of the License, or
     16 ;; (at your option) any later version.
     17 
     18 ;; GNU Emacs is distributed in the hope that it will be useful,
     19 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
     20 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     21 ;; GNU General Public License for more details.
     22 
     23 ;; You should have received a copy of the GNU General Public License
     24 ;; along with GNU Emacs.  If not, see <https://www.gnu.org/licenses/>.
     25 
     26 ;;; Commentary:
     27 
     28 ;; Org-Babel support for evaluating python source code.
     29 
     30 ;;; Code:
     31 
     32 (require 'org-macs)
     33 (org-assert-version)
     34 
     35 (require 'ob)
     36 (require 'org-macs)
     37 (require 'python)
     38 
     39 (declare-function py-shell "ext:python-mode" (&rest args))
     40 (declare-function py-choose-shell "ext:python-mode" (&optional shell))
     41 (declare-function py-shell-send-string "ext:python-mode" (strg &optional process))
     42 
     43 (defvar org-babel-tangle-lang-exts)
     44 (add-to-list 'org-babel-tangle-lang-exts '("python" . "py"))
     45 
     46 (defvar org-babel-default-header-args:python '())
     47 
     48 (defcustom org-babel-python-command "python"
     49   "Name of the command for executing Python code."
     50   :version "24.4"
     51   :package-version '(Org . "8.0")
     52   :group 'org-babel
     53   :type 'string)
     54 
     55 (defcustom org-babel-python-mode
     56   (if (featurep 'python-mode) 'python-mode 'python)
     57   "Preferred python mode for use in running python interactively.
     58 This will typically be either `python' or `python-mode'."
     59   :group 'org-babel
     60   :version "24.4"
     61   :package-version '(Org . "8.0")
     62   :type 'symbol)
     63 
     64 (defcustom org-babel-python-hline-to "None"
     65   "Replace hlines in incoming tables with this when translating to python."
     66   :group 'org-babel
     67   :version "24.4"
     68   :package-version '(Org . "8.0")
     69   :type 'string)
     70 
     71 (defcustom org-babel-python-None-to 'hline
     72   "Replace `None' in python tables with this before returning."
     73   :group 'org-babel
     74   :version "24.4"
     75   :package-version '(Org . "8.0")
     76   :type 'symbol)
     77 
     78 (defun org-babel-execute:python (body params)
     79   "Execute a block of Python code with Babel.
     80 This function is called by `org-babel-execute-src-block'."
     81   (let* ((org-babel-python-command
     82 	  (or (cdr (assq :python params))
     83 	      org-babel-python-command))
     84 	 (session (org-babel-python-initiate-session
     85 		   (cdr (assq :session params))))
     86          (result-params (cdr (assq :result-params params)))
     87          (result-type (cdr (assq :result-type params)))
     88 	 (return-val (when (eq result-type 'value)
     89 		       (cdr (assq :return params))))
     90 	 (preamble (cdr (assq :preamble params)))
     91 	 (async (org-babel-comint-use-async params))
     92          (full-body
     93 	  (concat
     94 	   (org-babel-expand-body:generic
     95 	    body params
     96 	    (org-babel-variable-assignments:python params))
     97 	   (when return-val
     98 	     (format (if session "\n%s" "\nreturn %s") return-val))))
     99          (result (org-babel-python-evaluate
    100 		  session full-body result-type
    101 		  result-params preamble async)))
    102     (org-babel-reassemble-table
    103      result
    104      (org-babel-pick-name (cdr (assq :colname-names params))
    105 			  (cdr (assq :colnames params)))
    106      (org-babel-pick-name (cdr (assq :rowname-names params))
    107 			  (cdr (assq :rownames params))))))
    108 
    109 (defun org-babel-prep-session:python (session params)
    110   "Prepare SESSION according to the header arguments in PARAMS.
    111 VARS contains resolved variable references."
    112   (let* ((session (org-babel-python-initiate-session session))
    113 	 (var-lines
    114 	  (org-babel-variable-assignments:python params)))
    115     (org-babel-comint-in-buffer session
    116       (mapc (lambda (var)
    117               (end-of-line 1) (insert var) (comint-send-input)
    118               (org-babel-comint-wait-for-output session))
    119 	    var-lines))
    120     session))
    121 
    122 (defun org-babel-load-session:python (session body params)
    123   "Load BODY into SESSION."
    124   (save-window-excursion
    125     (let ((buffer (org-babel-prep-session:python session params)))
    126       (with-current-buffer buffer
    127         (goto-char (process-mark (get-buffer-process (current-buffer))))
    128         (insert (org-babel-chomp body)))
    129       buffer)))
    130 
    131 ;; helper functions
    132 
    133 (defun org-babel-variable-assignments:python (params)
    134   "Return a list of Python statements assigning the block's variables."
    135   (mapcar
    136    (lambda (pair)
    137      (format "%s=%s"
    138 	     (car pair)
    139 	     (org-babel-python-var-to-python (cdr pair))))
    140    (org-babel--get-vars params)))
    141 
    142 (defun org-babel-python-var-to-python (var)
    143   "Convert an elisp value to a python variable.
    144 Convert an elisp value, VAR, into a string of python source code
    145 specifying a variable of the same value."
    146   (if (listp var)
    147       (concat "[" (mapconcat #'org-babel-python-var-to-python var ", ") "]")
    148     (if (eq var 'hline)
    149 	org-babel-python-hline-to
    150       (format
    151        (if (and (stringp var) (string-match "[\n\r]" var)) "\"\"%S\"\"" "%S")
    152        (if (stringp var) (substring-no-properties var) var)))))
    153 
    154 (defun org-babel-python-table-or-string (results)
    155   "Convert RESULTS into an appropriate elisp value.
    156 If the results look like a list or tuple, then convert them into an
    157 Emacs-lisp table, otherwise return the results as a string."
    158   (let ((res (org-babel-script-escape results)))
    159     (if (listp res)
    160         (mapcar (lambda (el) (if (eq el 'None)
    161                                  org-babel-python-None-to el))
    162                 res)
    163       res)))
    164 
    165 (defvar org-babel-python-buffers '((:default . "*Python*")))
    166 
    167 (defun org-babel-python-session-buffer (session)
    168   "Return the buffer associated with SESSION."
    169   (cdr (assoc session org-babel-python-buffers)))
    170 
    171 (defun org-babel-python-with-earmuffs (session)
    172   (let ((name (if (stringp session) session (format "%s" session))))
    173     (if (and (string= "*" (substring name 0 1))
    174 	     (string= "*" (substring name (- (length name) 1))))
    175 	name
    176       (format "*%s*" name))))
    177 
    178 (defun org-babel-python-without-earmuffs (session)
    179   (let ((name (if (stringp session) session (format "%s" session))))
    180     (if (and (string= "*" (substring name 0 1))
    181 	     (string= "*" (substring name (- (length name) 1))))
    182 	(substring name 1 (- (length name) 1))
    183       name)))
    184 
    185 (defvar py-which-bufname)
    186 (defvar python-shell-buffer-name)
    187 (defvar-local org-babel-python--initialized nil
    188   "Flag used to mark that python session has been initialized.")
    189 (defun org-babel-python-initiate-session-by-key (&optional session)
    190   "Initiate a python session.
    191 If there is not a current inferior-process-buffer in SESSION
    192 then create.  Return the initialized session."
    193   (save-window-excursion
    194     (let* ((session (if session (intern session) :default))
    195            (py-buffer (org-babel-python-session-buffer session))
    196 	   (cmd (if (member system-type '(cygwin windows-nt ms-dos))
    197 		    (concat org-babel-python-command " -i")
    198 		  org-babel-python-command)))
    199       (cond
    200        ((eq 'python org-babel-python-mode) ; python.el
    201 	(unless py-buffer
    202 	  (setq py-buffer (org-babel-python-with-earmuffs session)))
    203 	(let ((python-shell-buffer-name
    204 	       (org-babel-python-without-earmuffs py-buffer)))
    205 	  (run-python cmd)
    206           (with-current-buffer py-buffer
    207             (add-hook
    208              'python-shell-first-prompt-hook
    209              (lambda ()
    210                (setq-local org-babel-python--initialized t)
    211                (message "I am running!!!"))
    212              nil 'local))))
    213        ((and (eq 'python-mode org-babel-python-mode)
    214 	     (fboundp 'py-shell)) ; python-mode.el
    215 	(require 'python-mode)
    216 	;; Make sure that py-which-bufname is initialized, as otherwise
    217 	;; it will be overwritten the first time a Python buffer is
    218 	;; created.
    219 	(py-choose-shell)
    220 	;; `py-shell' creates a buffer whose name is the value of
    221 	;; `py-which-bufname' with '*'s at the beginning and end
    222 	(let* ((bufname (if (and py-buffer (buffer-live-p py-buffer))
    223 			    (replace-regexp-in-string ;; zap surrounding *
    224 			     "^\\*\\([^*]+\\)\\*$" "\\1" py-buffer)
    225 			  (concat "Python-" (symbol-name session))))
    226 	       (py-which-bufname bufname))
    227 	  (setq py-buffer (org-babel-python-with-earmuffs bufname))
    228 	  (py-shell nil nil t org-babel-python-command py-buffer nil nil t nil)))
    229        (t
    230 	(error "No function available for running an inferior Python")))
    231       ;; Wait until Python initializes.
    232       (if (eq 'python org-babel-python-mode) ; python.el
    233           ;; This is more reliable compared to
    234           ;; `org-babel-comint-wait-for-output' as python may emit
    235           ;; multiple prompts during initialization.
    236           (with-current-buffer py-buffer
    237             (while (not org-babel-python--initialized)
    238               (org-babel-comint-wait-for-output py-buffer)))
    239         (org-babel-comint-wait-for-output py-buffer))
    240       (setq org-babel-python-buffers
    241 	    (cons (cons session py-buffer)
    242 		  (assq-delete-all session org-babel-python-buffers)))
    243       session)))
    244 
    245 (defun org-babel-python-initiate-session (&optional session _params)
    246   "Create a session named SESSION according to PARAMS."
    247   (unless (string= session "none")
    248     (org-babel-python-session-buffer
    249      (org-babel-python-initiate-session-by-key session))))
    250 
    251 (defvar org-babel-python-eoe-indicator "org_babel_python_eoe"
    252   "A string to indicate that evaluation has completed.")
    253 
    254 (defconst org-babel-python-wrapper-method
    255   "
    256 def main():
    257 %s
    258 
    259 open('%s', 'w').write( str(main()) )")
    260 (defconst org-babel-python-pp-wrapper-method
    261   "
    262 import pprint
    263 def main():
    264 %s
    265 
    266 open('%s', 'w').write( pprint.pformat(main()) )")
    267 
    268 (defconst org-babel-python--exec-tmpfile "\
    269 with open('%s') as __org_babel_python_tmpfile:
    270     exec(compile(__org_babel_python_tmpfile.read(), __org_babel_python_tmpfile.name, 'exec'))"
    271   "Template for Python session command with output results.
    272 
    273 Has a single %s escape, the tempfile containing the source code
    274 to evaluate.")
    275 
    276 (defun org-babel-python-format-session-value
    277     (src-file result-file result-params)
    278   "Return Python code to evaluate SRC-FILE and write result to RESULT-FILE."
    279   (format "\
    280 import ast
    281 with open('%s') as __org_babel_python_tmpfile:
    282     __org_babel_python_ast = ast.parse(__org_babel_python_tmpfile.read())
    283 __org_babel_python_final = __org_babel_python_ast.body[-1]
    284 if isinstance(__org_babel_python_final, ast.Expr):
    285     __org_babel_python_ast.body = __org_babel_python_ast.body[:-1]
    286     exec(compile(__org_babel_python_ast, '<string>', 'exec'))
    287     __org_babel_python_final = eval(compile(ast.Expression(
    288         __org_babel_python_final.value), '<string>', 'eval'))
    289     with open('%s', 'w') as __org_babel_python_tmpfile:
    290         if %s:
    291             import pprint
    292             __org_babel_python_tmpfile.write(pprint.pformat(__org_babel_python_final))
    293         else:
    294             __org_babel_python_tmpfile.write(str(__org_babel_python_final))
    295 else:
    296     exec(compile(__org_babel_python_ast, '<string>', 'exec'))
    297     __org_babel_python_final = None"
    298 	  (org-babel-process-file-name src-file 'noquote)
    299 	  (org-babel-process-file-name result-file 'noquote)
    300 	  (if (member "pp" result-params) "True" "False")))
    301 
    302 (defun org-babel-python-evaluate
    303     (session body &optional result-type result-params preamble async)
    304   "Evaluate BODY as Python code."
    305   (if session
    306       (if async
    307 	  (org-babel-python-async-evaluate-session
    308 	   session body result-type result-params)
    309 	(org-babel-python-evaluate-session
    310 	 session body result-type result-params))
    311     (org-babel-python-evaluate-external-process
    312      body result-type result-params preamble)))
    313 
    314 (defun org-babel-python--shift-right (body &optional count)
    315   (with-temp-buffer
    316     (python-mode)
    317     (insert body)
    318     (goto-char (point-min))
    319     (while (not (eobp))
    320       (unless (python-syntax-context 'string)
    321 	(python-indent-shift-right (line-beginning-position)
    322 				   (line-end-position)
    323 				   count))
    324       (forward-line 1))
    325     (buffer-string)))
    326 
    327 (defun org-babel-python-evaluate-external-process
    328     (body &optional result-type result-params preamble)
    329   "Evaluate BODY in external python process.
    330 If RESULT-TYPE equals `output' then return standard output as a
    331 string.  If RESULT-TYPE equals `value' then return the value of the
    332 last statement in BODY, as elisp."
    333   (let ((raw
    334          (pcase result-type
    335            (`output (org-babel-eval org-babel-python-command
    336 				    (concat preamble (and preamble "\n")
    337 					    body)))
    338            (`value (let ((tmp-file (org-babel-temp-file "python-")))
    339 		     (org-babel-eval
    340 		      org-babel-python-command
    341 		      (concat
    342 		       preamble (and preamble "\n")
    343 		       (format
    344 			(if (member "pp" result-params)
    345 			    org-babel-python-pp-wrapper-method
    346 			  org-babel-python-wrapper-method)
    347 			(org-babel-python--shift-right body)
    348 			(org-babel-process-file-name tmp-file 'noquote))))
    349 		     (org-babel-eval-read-file tmp-file))))))
    350     (org-babel-result-cond result-params
    351       raw
    352       (org-babel-python-table-or-string (org-trim raw)))))
    353 
    354 (defun org-babel-python--send-string (session body)
    355   "Pass BODY to the Python process in SESSION.
    356 Return output."
    357   (with-current-buffer session
    358     (let* ((string-buffer "")
    359 	   (comint-output-filter-functions
    360 	    (cons (lambda (text) (setq string-buffer
    361 				       (concat string-buffer text)))
    362 		  comint-output-filter-functions))
    363 	   (body (format "\
    364 try:
    365 %s
    366 except:
    367     raise
    368 finally:
    369     print('%s')"
    370 			 (org-babel-python--shift-right body 4)
    371 			 org-babel-python-eoe-indicator)))
    372       (if (not (eq 'python-mode org-babel-python-mode))
    373 	  (let ((python-shell-buffer-name
    374 		 (org-babel-python-without-earmuffs session)))
    375 	    (python-shell-send-string body))
    376 	(require 'python-mode)
    377 	(py-shell-send-string body (get-buffer-process session)))
    378       ;; same as `python-shell-comint-end-of-output-p' in emacs-25.1+
    379       (while (not (string-match
    380 		   org-babel-python-eoe-indicator
    381 		   string-buffer))
    382 	(accept-process-output (get-buffer-process (current-buffer))))
    383       (org-babel-chomp (substring string-buffer 0 (match-beginning 0))))))
    384 
    385 (defun org-babel-python-evaluate-session
    386     (session body &optional result-type result-params)
    387   "Pass BODY to the Python process in SESSION.
    388 If RESULT-TYPE equals `output' then return standard output as a
    389 string.  If RESULT-TYPE equals `value' then return the value of the
    390 last statement in BODY, as elisp."
    391   (let* ((tmp-src-file (org-babel-temp-file "python-"))
    392          (results
    393 	  (progn
    394 	    (with-temp-file tmp-src-file (insert body))
    395             (pcase result-type
    396 	      (`output
    397 	       (let ((body (format org-babel-python--exec-tmpfile
    398 				   (org-babel-process-file-name
    399 				    tmp-src-file 'noquote))))
    400 		 (org-babel-python--send-string session body)))
    401               (`value
    402                (let* ((tmp-results-file (org-babel-temp-file "python-"))
    403 		      (body (org-babel-python-format-session-value
    404 			     tmp-src-file tmp-results-file result-params)))
    405 		 (org-babel-python--send-string session body)
    406 		 (sleep-for 0 10)
    407 		 (org-babel-eval-read-file tmp-results-file)))))))
    408     (org-babel-result-cond result-params
    409       results
    410       (org-babel-python-table-or-string results))))
    411 
    412 (defun org-babel-python-read-string (string)
    413   "Strip \\='s from around Python string."
    414   (if (and (string-prefix-p "'" string)
    415 	   (string-suffix-p "'" string))
    416       (substring string 1 -1)
    417     string))
    418 
    419 ;; Async session eval
    420 
    421 (defconst org-babel-python-async-indicator "print ('ob_comint_async_python_%s_%s')")
    422 
    423 (defun org-babel-python-async-value-callback (params tmp-file)
    424   (let ((result-params (cdr (assq :result-params params)))
    425 	(results (org-babel-eval-read-file tmp-file)))
    426     (org-babel-result-cond result-params
    427       results
    428       (org-babel-python-table-or-string results))))
    429 
    430 (defun org-babel-python-async-evaluate-session
    431     (session body &optional result-type result-params)
    432   "Asynchronously evaluate BODY in SESSION.
    433 Returns a placeholder string for insertion, to later be replaced
    434 by `org-babel-comint-async-filter'."
    435   (org-babel-comint-async-register
    436    session (current-buffer)
    437    "ob_comint_async_python_\\(.+\\)_\\(.+\\)"
    438    'org-babel-chomp 'org-babel-python-async-value-callback)
    439   (let ((python-shell-buffer-name (org-babel-python-without-earmuffs session)))
    440     (pcase result-type
    441       (`output
    442        (let ((uuid (md5 (number-to-string (random 100000000)))))
    443          (with-temp-buffer
    444            (insert (format org-babel-python-async-indicator "start" uuid))
    445            (insert "\n")
    446            (insert body)
    447            (insert "\n")
    448            (insert (format org-babel-python-async-indicator "end" uuid))
    449            (python-shell-send-buffer))
    450          uuid))
    451       (`value
    452        (let ((tmp-results-file (org-babel-temp-file "python-"))
    453              (tmp-src-file (org-babel-temp-file "python-")))
    454          (with-temp-file tmp-src-file (insert body))
    455          (with-temp-buffer
    456            (insert (org-babel-python-format-session-value tmp-src-file tmp-results-file result-params))
    457            (insert "\n")
    458            (insert (format org-babel-python-async-indicator "file" tmp-results-file))
    459            (python-shell-send-buffer))
    460          tmp-results-file)))))
    461 
    462 (provide 'ob-python)
    463 
    464 ;;; ob-python.el ends here