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)))

Reply via email to