0

Quarto is a literate programming framework. It allows you to combine in a single file code (in various programming languages) and text (in markdown).*

When a Quarto document is rendered, the output from the code is combined with the text into a single document (e.g. pdf or html).

I have installed Quarto-emacs, which does a great job of rendering and live-previewing quarto files. Among other things, I have also installed Polymode, which associates different code and text blocks with their respective major-modes in a single buffer. Now I can write, code, and preview the results on the fly. Thanks, Emacs!

Literate programming with live preview side-by-side

However, whenever I change some python code block, I have to re-render the whole document (by running markdown-preview or markdown-compile). This can take a while. Moreover, I can't run the code block and then continue to experiment in the interpreter (REPL).

So what I want is to run only a selected python code block or the selected and all previous code blocks (like in a Jupyter notebook) in the interpreter (REPL). This works quite well in RStudio, but I would rather stick to Emacs, which in general has great support for Python and R.

It sort of works with R code blocks in Emacs. When I hit C-c b (ess-eval-buffer) on an R code block, a buffer with an R console is opened and the block is executed there. I don't know how to evaluate all previous code blocks, however.

But when I hit C-c c (elpy-shell-send-buffer), the whole buffer is evaluated instead of just the present code block. Of course, this results in a syntax error. When I try C-c C-y w elpy-shell-send-codecell, I get "Not in a codecell".

I also tried several polymode functions:

  • polymode-eval-region-or-chunk produces Undefined ‘polymode-eval-region-function’ in buffer test.qmd[python] on python blocks but works on R blocks.
  • polymode-eval-buffer, polymode-eval-buffer-from-beg-to-point, and polymode-eval-buffer-from-point-to-end only run the R blocks, not the Python blocks.

Is there any way to make this work as seamlessly as in RStudio?

*This is a generalization of R Markdown and similar to both org-mode with org-babel and Juypter notebooks, which Emacs can display and run using ein.

9
  • You mention that you can 'preview results on the fly'? So why you (also) have to re-render the document using markdown-preview? According to the Quarto documentation you should only have to save the file to update the 'preview'. Nov 15, 2022 at 8:59
  • @dalanicolai Quarto-emacs doesn't preview on file save. See here: github.com/quarto-dev/quarto-emacs/issues/5
    – henning
    Nov 15, 2022 at 9:24
  • 1
    Ah yeah, I am just testing now, and noticed it. However, the 'preview update on save' does work when starting the preview from the terminal (that is not so much work, and you only have to do it once). It is a good workaround for now. Nov 15, 2022 at 9:41
  • You mean that ess-eval-buffer works from the .qmd file buffer? Here it does open an R console, but it sends the whole buffer contents instead of the only code block. Nov 15, 2022 at 10:07
  • Okay, it got fixed after installing poly-R separately. Nov 15, 2022 at 10:14

1 Answer 1

1
+300

EDIT

Probably a cleaner way than the original answer is to just define a suitable polymode-eval-region-function. Inspecting again poly-R.el, we find that we can define an appropriate function as follows:

(defun poly-python-eval-region (beg end msg)
    (python-shell-send-region beg end nil msg t))

(defun poly-python-mode-setup ()
  (setq-local polymode-eval-region-function #'poly-python-eval-region))

(add-hook 'python-mode-hook #'poly-python-mode-setup)

Make sure to evaluate the above code before opening the quarto file, as it configures the polymode-eval-region-function via the python-mode-hook.

Also, make sure that you start a python shell (by placing your cursor within the python cell and doing M-x run-python) before calling polymode-eval-chunk.

Subsequently, you should do M-x polymode-eval-chunk from within a python code block, and then Emacs will correctly make use of the poly-python-eval-region function.

END EDIT

Although I haven't tested this with elpy, I guess it should work when using elpy also. From poly-R.el, we find that the region is narrowed by advising ess-eval-buffer (and similar functions) with pm-execute-narrowed-to-span.

However, we can not simply advise python-shell-send-buffer because the save-restriction within that function breaks the 'effect' of the advice. We can solve it by defining a custom my-python-shell-send-buffer:

(defun my-python-shell-send-buffer (&optional send-main msg)
  "See docstring python-shell-send-buffer."
  (interactive (list current-prefix-arg t))
  (python-shell-send-region (point-min) (point-max) send-main msg t))

(advice-add 'my-python-shell-send-buffer :around 'pm-execute-narrowed-to-span)

Now, with the cursor placed inside a python code block, we can simply send the block via M-x my-python-shell-send-buffer (or bind it to some shortcut).

Before we can use this, we first have to do M-x run-python from the .qmd file buffer (but I guess you know that already).

Beware that python-shell-send-buffer only prints output (i.e. from print statements), not return values. But, when sending your example code block, the plot should show 'pop-up '.

13
  • 1
    Many thanks for this. I won't be able to test this before the week-end, but I'll accept the answer, of course, if it works
    – henning
    Nov 16, 2022 at 7:50
  • Again, thanks for your suggestion! Unfortunately, the original solution (my-python-shell-send-buffer) results in: (anonymous nil t): Wrong number of arguments: (2 . 4), 5 [M-x pm-debug-mode RET for more info] The new solution results in: poly-python-eval-region: Wrong number of arguments: (2 . 4), 5.
    – henning
    Nov 19, 2022 at 21:26
  • Strangely, for the new solution poly-python-eval-region actually remains unchanged. I get The mark is not set now, so there is no region. If instead I call poly-mode-eval-chunk, i get poly-python-eval-region: Wrong number of arguments: (2 . 4), 5, as described above.
    – henning
    Nov 19, 2022 at 21:32
  • I'm sure this is my mistake. Too bad it's not obvious for me how to fix it.
    – henning
    Nov 19, 2022 at 21:32
  • 1
    Never mind, I figured out the hook has to be called first. Here's what worked for me: I left two of the arguments out of the call to python-shell-send-region within your first function, e.g. I changed it from (python-shell-send-region beg end nil msg t) to (python-shell-send-region beg end msg)). Thanks so much for solving the first 99% for me!
    – henning
    Nov 20, 2022 at 17:26

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.