OK, so what was all the stuff I needed to tell org-babel to do specially here?
First off, org needed to be able to communicate to the Python session the name
of the file to write the plot to. I do this by making the whole plist for this
org-babel snippet available to python:
;; THIS advice makes all the org-babel parameters available to python in the
;; _org_babel_params dict. I care about _org_babel_params['_file'] specifically,
;; but everything is available
(defun dima-org-babel-python-var-to-python (var)
"Convert an elisp value to a python variable.
Like the original, but supports (a . b) cells and symbols
"
(if (listp var)
(if (listp (cdr var))
(concat "[" (mapconcat #'org-babel-python-var-to-python var ", ") "]")
(format "\"\"\"%s\"\"\"" var))
(if (symbolp var)
(format "\"\"\"%s\"\"\"" var)
(if (eq var 'hline)
org-babel-python-hline-to
(format
(if (and (stringp var) (string-match "[\n\r]" var)) "\"\"%S\"\"" "%S")
(if (stringp var) (substring-no-properties var) var))))))
(defun dima-alist-to-python-dict (alist)
"Generates a string defining a python dict from the given alist"
(let ((keyvalue-list
(mapcar (lambda (x)
(format "%s = %s, "
(replace-regexp-in-string
"[^a-zA-Z0-9_]" "_"
(symbol-name (car x)))
(dima-org-babel-python-var-to-python (cdr x))))
alist)))
(concat
"dict( "
(apply 'concat keyvalue-list)
")")))
(defun dima-org-babel-python-pass-all-params (f params)
(cons
(concat
"_org_babel_params = "
(dima-alist-to-python-dict params))
(funcall f params)))
(unless
(advice-member-p
#'dima-org-babel-python-pass-all-params
#'org-babel-variable-assignments:python)
(advice-add
#'org-babel-variable-assignments:python
:around #'dima-org-babel-python-pass-all-params))
So if there's a
:file
plist key, the python code can grab that, and write the
plot to that filename. But I don't really want to specify an output file for
every single org-babel snippet. All I really care about is that each plot gets a
unique filename. So I omit the
:file
key entirely, and use this advice to
generate one for me:
;; This sets a default :file tag, set to a unique filename. I want each demo to
;; produce an image, but I don't care what it is called. I omit the :file tag
;; completely, and this advice takes care of it
(defun dima-org-babel-python-unique-plot-filename
(f &optional arg info params)
(funcall f arg info
(cons (cons ':file
(format "guide-%d.svg"
(condition-case nil
(setq dima-unique-plot-number (1+ dima-unique-plot-number))
(error (setq dima-unique-plot-number 0)))))
params)))
(unless
(advice-member-p
#'dima-org-babel-python-unique-plot-filename
#'org-babel-execute-src-block)
(advice-add
#'org-babel-execute-src-block
:around #'dima-org-babel-python-unique-plot-filename))
This uses the
dima-unique-plot-number
integer to keep track of each plot. I
increment this with each plot. Getting closer. It isn't strictly required, but
it'd be nice if each plot had the same output filename each time I generated it.
So I want to reset the plot number to 0 each time:
;; If I'm regenerating ALL the plots, I start counting the plots from 0
(defun dima-reset-unique-plot-number
(&rest args)
(setq dima-unique-plot-number 0))
(unless
(advice-member-p
#'dima-reset-unique-plot-number
#'org-babel-execute-buffer)
(advice-add
#'org-babel-execute-buffer
:after #'dima-reset-unique-plot-number))
Finally, I want to lie to the user a little bit. The code I'm actually
executing writes each plot to an .svg. But the code I'd like the user to see
should use the default output: an interactive, graphical window. I do that by
tweaking the python session to tell the
gnuplotlib
object to write to .svg
files from org by default, instead of using the graphical terminal:
;; I'm using github to display guide.org, so I'm not using the "normal" org
;; exporter. I want the demo text to not contain the hardcopy= tags, but clearly
;; I need the hardcopy tag when generating the plots. I add some python to
;; override gnuplotlib.plot() to add the hardcopy tag somewhere where the reader
;; won't see it. But where to put this python override code? If I put it into an
;; org-babel block, it will be rendered, and the :export tags will be ignored,
;; since github doesn't respect those (probably). So I put the extra stuff into
;; an advice. Whew.
(defun dima-org-babel-python-set-demo-output (f body params)
(with-temp-buffer
(insert body)
(beginning-of-buffer)
(when (search-forward "import gnuplotlib as gp" nil t)
(end-of-line)
(insert
"\n"
"if not hasattr(gp.gnuplotlib, 'orig_init'):\n"
" gp.gnuplotlib.orig_init = gp.gnuplotlib.__init__\n"
"gp.gnuplotlib.__init__ = lambda self, *args, **kwargs: gp.gnuplotlib.orig_init(self, *args, hardcopy=_org_babel_params['_file'] if 'file' in _org_babel_params['_result_params'] else None, **kwargs)\n"))
(setq body (buffer-substring-no-properties (point-min) (point-max))))
(funcall f body params))
(unless
(advice-member-p
#'dima-org-babel-python-set-demo-output
#'org-babel-execute:python)
(advice-add
#'org-babel-execute:python
:around #'dima-org-babel-python-set-demo-output))
)
And that's it. The advises in the talk are slightly different, in uninteresting
ways. Some of this should be upstreamed to org-babel somehow. Now entirely clear
which part, but I'll cross that bridge when I get to it.