branch: externals/org
commit 2c81673be87f884688b97c89f7a71ded7a7da9b6
Author: Jack Kamm <[email protected]>
Commit: Jack Kamm <[email protected]>
ob-python: Move Python utilities into separate module
* etc/org-babel/ob_python_utils.py: Python module containing utilities
for `ob-python'
* etc/org-babel/.gitignore: Ignore file for Babel external language
modules
* .dir-locals.el: Prevent tabs in python-mode
* lisp/ob-python.el (org-babel-python--load-utils): New snippet to
load Python utilities for `ob-python'.
(org-babel-python--output-graphics-wrapper): Call Python utility
functions for graphics, avoiding loading matpotlib into namespace.
(org-babel-python--def-format-value): Remove Python function for
formatting results; it has been migrated into ob_python_utils.py.
(org-babel-python--setup-session): Replace
`org-babel-python--def-format-value' with
`org-babel-python--load-utils'.
(org-babel-python-format-session-value): Python code to parse and
evaluate source has been moved into ob_python_utils.py; call the new
function from that module.
(org-babel-python-evaluate-external-process): Replace
`org-babel-python--def-format-value' with
`org-babel-python--load-utils'. Replace function call that was
migrated to ob_python_utils.py.
(org-babel-python-evaluate-session): Python code was migrated to a new
function in ob_python_utils.py; call that function.
---
.dir-locals.el | 1 +
etc/org-babel/.gitignore | 1 +
etc/org-babel/ob_python_utils.py | 104 +++++++++++++++++++++++++++++++++++++++
lisp/ob-python.el | 98 ++++++++++++------------------------
4 files changed, 137 insertions(+), 67 deletions(-)
diff --git a/.dir-locals.el b/.dir-locals.el
index 452ee043291..afc6b7e5d74 100644
--- a/.dir-locals.el
+++ b/.dir-locals.el
@@ -15,6 +15,7 @@
(org-footnote-define-inline . nil)
(org-footnote-section . "Footnotes")
(org-hide-emphasis-markers . nil)))
+ (python-mode . ((indent-tabs-mode . nil)))
("testing/lisp" . ((emacs-lisp-mode . ((elisp-flymake-byte-compile-load-path
"./" "../../lisp/"))))))
diff --git a/etc/org-babel/.gitignore b/etc/org-babel/.gitignore
new file mode 100644
index 00000000000..bee8a64b79a
--- /dev/null
+++ b/etc/org-babel/.gitignore
@@ -0,0 +1 @@
+__pycache__
diff --git a/etc/org-babel/ob_python_utils.py b/etc/org-babel/ob_python_utils.py
new file mode 100644
index 00000000000..d4a78153747
--- /dev/null
+++ b/etc/org-babel/ob_python_utils.py
@@ -0,0 +1,104 @@
+# Python utilities for ob-python.el
+#
+# Copyright (C) 2026 Free Software Foundation, Inc.
+#
+# This file is part of GNU Emacs.
+#
+# GNU Emacs is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# GNU Emacs is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>.
+
+import ast
+import pprint
+
+def session_results_output(
+ src_file_name, context
+):
+ with open(src_file_name) as f:
+ exec(compile(f.read(), f.name, 'exec'), context)
+
+def session_results_value(
+ src_file_name, result_file_name, result_params, context
+):
+ with open(src_file_name) as f:
+ src_ast = ast.parse(f.read())
+
+ final_line = src_ast.body[-1]
+
+ if isinstance(final_line, ast.Expr):
+ src_ast.body = src_ast.body[:-1]
+ exec(compile(src_ast, '<string>', 'exec'), context)
+ final_line = eval(compile(ast.Expression(
+ final_line.value), '<string>', 'eval'), context)
+ else:
+ exec(compile(src_ast, '<string>', 'exec'), context)
+ final_line = None
+
+ format_results_value(
+ final_line,
+ result_file_name,
+ result_params
+ )
+
+def clear_graphics():
+ import matplotlib.pyplot
+ matplotlib.pyplot.gcf().clear()
+
+def save_graphics(file_name):
+ import matplotlib.pyplot
+ matplotlib.pyplot.savefig(file_name)
+
+def format_results_value(result, result_file_name, result_params):
+ with open(result_file_name, 'w') as result_file_obj:
+ if 'graphics' in result_params:
+ result.savefig(result_file_name)
+ elif 'pp' in result_params:
+ result_file_obj.write(pprint.pformat(result))
+ elif 'list' in result_params and isinstance(result, dict):
+ result_file_obj.write(
+ str(['{} :: {}'.format(k, v) for k, v in result.items()])
+ )
+ else:
+ if not set(result_params).intersection(['scalar', 'verbatim',
'raw']):
+ def dict2table(res):
+ if isinstance(res, dict):
+ return [(k, dict2table(v)) for k, v in res.items()]
+ elif isinstance(res, list) or isinstance(res, tuple):
+ return [dict2table(x) for x in res]
+ else:
+ return res
+ if 'table' in result_params:
+ result = dict2table(result)
+ try:
+ import pandas
+ except ImportError:
+ pass
+ else:
+ if isinstance(result, pandas.DataFrame) and 'table' in
result_params:
+ result = [
+ [result.index.name or ''] + list(result.columns)
+ ] + [None] + [
+ [i] + list(row) for i, row in result.iterrows()
+ ]
+ elif isinstance(result, pandas.Series) and 'table' in
result_params:
+ result = list(result.items())
+ try:
+ import numpy
+ except ImportError:
+ pass
+ else:
+ if isinstance(result, numpy.ndarray):
+ if 'table' in result_params:
+ result = result.tolist()
+ else:
+ result = repr(result)
+ result_file_obj.write(str(result))
diff --git a/lisp/ob-python.el b/lisp/ob-python.el
index 1cb683492e1..60211cf148b 100644
--- a/lisp/ob-python.el
+++ b/lisp/ob-python.el
@@ -149,60 +149,38 @@ VARS contains resolved variable references."
;; helper functions
+(defconst org-babel-python--load-utils
+ (format "\
+def __OB_PYTHON_setup_path(dir):
+ import sys
+ if not dir in sys.path:
+ sys.path.append(dir)
+__OB_PYTHON_setup_path('%s')
+import ob_python_utils as __OB_PYTHON"
+ (let ((ob-root (file-name-directory (locate-library "ob"))))
+ (cond
+ ;; First check whether it looks like we're running from the main
+ ;; Org repository.
+ ((let ((etc-org (expand-file-name "../etc/org-babel/" ob-root)))
+ (and (file-directory-p etc-org) etc-org)))
+ ;; Next look for the directory alongside ob.el because package.el
+ ;; and straight will put all of org-mode/lisp/ in org-mode/.
+ ((let ((etc-pkg (expand-file-name "etc/org-babel/" ob-root)))
+ (and (file-directory-p etc-pkg) etc-pkg)))
+ ;; Finally fall back the location used by shared system installs
+ ;; and when running directly from Emacs repository.
+ (t
+ (expand-file-name "org/org-babel/" data-directory)))))
+ "Python code snippet to load utilities for `ob-python'.")
+
(defconst org-babel-python--output-graphics-wrapper "\
-import matplotlib.pyplot
-matplotlib.pyplot.gcf().clear()
+__OB_PYTHON.clear_graphics()
%s
-matplotlib.pyplot.savefig('%s')"
+__OB_PYTHON.save_graphics('%s')"
"Format string for saving Python graphical output.
Has two %s escapes, for the Python code to be evaluated, and the
file to save the graphics to.")
-(defconst org-babel-python--def-format-value "\
-def __org_babel_python_format_value(result, result_file, result_params):
- with open(result_file, 'w') as __org_babel_python_tmpfile:
- if 'graphics' in result_params:
- result.savefig(result_file)
- elif 'pp' in result_params:
- import pprint
- __org_babel_python_tmpfile.write(pprint.pformat(result))
- elif 'list' in result_params and isinstance(result, dict):
- __org_babel_python_tmpfile.write(str(['{} :: {}'.format(k, v) for
k, v in result.items()]))
- else:
- if not set(result_params).intersection(\
-['scalar', 'verbatim', 'raw']):
- def dict2table(res):
- if isinstance(res, dict):
- return [(k, dict2table(v)) for k, v in res.items()]
- elif isinstance(res, list) or isinstance(res, tuple):
- return [dict2table(x) for x in res]
- else:
- return res
- if 'table' in result_params:
- result = dict2table(result)
- try:
- import pandas
- except ImportError:
- pass
- else:
- if isinstance(result, pandas.DataFrame) and 'table' in
result_params:
- result = [[result.index.name or ''] +
list(result.columns)] + \
-[None] + [[i] + list(row) for i, row in result.iterrows()]
- elif isinstance(result, pandas.Series) and 'table' in
result_params:
- result = list(result.items())
- try:
- import numpy
- except ImportError:
- pass
- else:
- if isinstance(result, numpy.ndarray):
- if 'table' in result_params:
- result = result.tolist()
- else:
- result = repr(result)
- __org_babel_python_tmpfile.write(str(result))"
- "Python function to format value result and save it to file.")
-
(defun org-babel-variable-assignments:python (params)
"Return a list of Python statements assigning the block's variables.
The assignments are defined in PARAMS."
@@ -297,7 +275,7 @@ IS-SESSION, this might return nil, which means to use
Function should be run from within the Python session buffer.
This is often run as a part of `python-shell-first-prompt-hook',
unless the Python session was created outside Org."
- (python-shell-send-string-no-output org-babel-python--def-format-value)
+ (python-shell-send-string-no-output org-babel-python--load-utils)
(setq-local org-babel-python--initialized t))
(defun org-babel-python-initiate-session-by-key (&optional session)
"Initiate a python session.
@@ -374,19 +352,7 @@ already been configured as such, do nothing."
"Return Python code to evaluate SRC-FILE and write result to RESULT-FILE.
RESULT-PARAMS defines the result type."
(format "\
-import ast
-with open('%s') as __org_babel_python_tmpfile:
- __org_babel_python_ast = ast.parse(__org_babel_python_tmpfile.read())
-__org_babel_python_final = __org_babel_python_ast.body[-1]
-if isinstance(__org_babel_python_final, ast.Expr):
- __org_babel_python_ast.body = __org_babel_python_ast.body[:-1]
- exec(compile(__org_babel_python_ast, '<string>', 'exec'))
- __org_babel_python_final = eval(compile(ast.Expression(
- __org_babel_python_final.value), '<string>', 'eval'))
-else:
- exec(compile(__org_babel_python_ast, '<string>', 'exec'))
- __org_babel_python_final = None
-__org_babel_python_format_value(__org_babel_python_final, '%s', %s)"
+__OB_PYTHON.session_results_value('%s', '%s', %s, globals())"
(org-babel-process-file-name src-file 'noquote)
(org-babel-process-file-name result-file 'noquote)
(org-babel-python-var-to-python result-params)))
@@ -437,11 +403,11 @@ non-nil, then save graphical results to that file
instead."
(concat
preamble (and preamble "\n")
(format
- (concat org-babel-python--def-format-value "
+ (concat org-babel-python--load-utils "
def main():
%s
-__org_babel_python_format_value(main(), '%s', %s)")
+__OB_PYTHON.format_results_value(main(), '%s', %s)")
(org-babel-python--shift-right body)
(org-babel-process-file-name results-file 'noquote)
(org-babel-python-var-to-python result-params))))
@@ -487,9 +453,7 @@ non-nil, then save graphical results to that file instead."
body)))
(pcase result-type
(`output
- (let ((body (format "\
-with open('%s') as __org_babel_python_tmpfile:
- exec(compile(__org_babel_python_tmpfile.read(),
__org_babel_python_tmpfile.name, 'exec'))"
+ (let ((body (format "__OB_PYTHON.session_results_output('%s',
globals())"
(org-babel-process-file-name
tmp-src-file 'noquote))))
(org-babel-python-send-string session body)))