jupyter icon indicating copy to clipboard operation
jupyter copied to clipboard

Remove empty #+RESULTS: when using async

Open timlod opened this issue 3 years ago • 4 comments

In long org-files with much code, there may be quite a few blocks that need to run, but don't contain results. For better flow when reading, I want to remove these dangling results. Ie.

begin_src python
end_src

should not have #+RESULTS: following execution of this (empty) block.

I know it's possible to use :results silent to suppress this, however,

  1. setting this manually for many blocks can be cumbersome
  2. in async computation, having the hash for intermediate output can be a useful indicator of which block is currently running

https://stackoverflow.com/questions/47585133/how-to-auto-suppress-results-for-empty-output contains a working solution to remove empty results, but it doesn't work when using :async yes.

I tried to find a solution that works in this case, but so far have been without luck. I haven't been able to find a reliable hook/function that is executed whenever a src block is "done", and edebug doesn't really work because of async. I find that if there is no result, most functions that I could add an advice to and check for empty results, are not run at all (ie. jupyter-org--insert-result).

Is there a way to achieve this?

timlod avatar Mar 24 '22 16:03 timlod

@timlod I don't know if you're still interested in this, but in case anyone else is, this is how you can do it:

(with-eval-after-load 'jupyter-client
  (defun /jupyter-remove-empty-async-results (args)
    (let*
        ((req (nth 1 args))
         (msg (nth 2 args)))
      (jupyter-with-message-content msg (status payload)
        (when (and (jupyter-org-request-async-p req)
                   (equal status "ok")
                   (not (jupyter-org-request-id-cleared-p req)))
          (jupyter-org--clear-request-id req)
          (org-with-point-at (jupyter-org-request-marker req)
            (org-babel-remove-result))))
      args))

  (unless (advice-member-p #'/jupyter-remove-empty-async-results 'jupyter-handle-execute-reply)
    (advice-add 'jupyter-handle-execute-reply :filter-args #'/jupyter-remove-empty-async-results))
  )

The way the async process seems to work is be first putting the #+RESULTS: marker there, then the async request id, then every time the async request generates a result, it adds it below the marker, every time making sure the async request id has been cleared. So if the async request id has not been cleared by the time the request has been completed that means there was no result. Which leads to the above solution of doing the deleting right before the async request id would be cleared in jupyter-handle-execute-reply at request completion.

It really would be cool if this could be included as an optional feature to customize, though.

MoritzMaxeiner avatar Jun 12 '23 19:06 MoritzMaxeiner

Hi, Thanks! I worked around it by adding I function to call before committing which removes all empty results:

(defun org-babel-remove-empty-results ()
  "Remove all #+RESULTS: without content from buffer."
  (interactive)
  (save-excursion
    (org-babel-map-executables nil
      (let ((res (org-babel-where-is-src-block-result))
            (del))
        (when res
          (save-excursion
            (goto-char res)
            (forward-line)
            (when (char-equal (char-after (point)) 10)
              (setq del t)))
          (when del
            (org-babel-remove-result)))))))

Your solution seems to work, however, if I execute code directly in the REPL instead of via org blocks, I get a very long message which starts with:

Jupyter: I/O subscriber error: (wrong-type-argument jupyter-org-request #s(jupyter-request "3bbf7eb2-917b-4f41-a4e7-46536af3163a" "execute_request" (:code "" :silent t :store_history t :user_expressions #s(hash-table size 1 test eql rehash-size 1.5 rehash-threshold 0.8125 data ()) :allow_stdin t :stop_on_error :json-false) #s(jupyter-org-client jupyter--clients (#<finalizer>) "busy" 25 (:status "ok" :protocol_version "5.3" :implementation "ipython" :implementation_version "8.9.0" :language_info (:name python :version "3.11.4" :mimetype "text/x-python" :codemirror_mode (:name "ipython" :version 3) :pygments_lexer "ipython3" :nbconvert_exporter "python" :file_extension ".py") :banner "Python 3.11.4 (main, Jun  7 2023, 12:45:48) [GCC 11.3.0]

Reason being that your function calls jupyter-org.. functions which I assume aren't available when entering through the REPL. I tried some avenues to get it to call only when we're entering through an org buffer, which was surprisingly tricky - although it's quite possible I missed something obvious.

Ultimately, I came up with this ugly workaround (with the help of ChatGPT actually to come up with something that gives me the list of visible buffers :)):

  (defun visible-buffers ()
    "Return a list of buffers that would be shown by `list-buffers'."
    (seq-filter (lambda (b) (not (string-prefix-p " " (buffer-name b)))) (buffer-list)))


  (defun /jupyter-remove-empty-async-results (args)
    (let* ((last-buffer-name
            ;; get the first non-current buffer from the visible buffer list
            (buffer-name (car (seq-filter (lambda (b) (not (eq b (current-buffer))))
                                          (visible-buffers)))))
           (req (nth 1 args))
           (msg (nth 2 args)))
      (with-current-buffer last-buffer-name
        (when (and (bound-and-true-p jupyter-org-interaction-mode)
                   (jupyter-org-request-async-p req)
                   (jupyter-with-message-content msg (status payload)
                     (equal status "ok")
                     (not (jupyter-org-request-id-cleared-p req))))
          (jupyter-org--clear-request-id req)
          (org-with-point-at (jupyter-org-request-marker req)
            (org-babel-remove-result)))))
    args)

My observation was that jupyter-org-interaction-mode will be active in the org buffer, but not in the REPL. Since the active buffer during jupyter-handle-execute-reply is some internal zmq buffer, I filter the list of buffers by the knowledge (which I didn't know but ChatGPT did) that these types of internal buffers usually have names that start with a space. The buffer before that will be org or REPL, which is why this works. If you have a better workaround, please let me know!

timlod avatar Jun 16 '23 08:06 timlod

Hey, I hadn't used the REPL yet, but when I did I ran into the same error.

The problem is that I'm advising the generic function, which is overloaded by both the repl and org implementation. Since there is no way I know of to advise the implementations themselves, instead of the generic function, we do indeed need to work around that.

Fortunately, there is an easy workaround, because the request parameter has a different type for repl and org requests, so the fixed version looks like this:

(with-eval-after-load 'jupyter-client
  (defun /jupyter-remove-empty-async-results (args)
    (let*
        ((req (nth 1 args))
         (msg (nth 2 args))
         (is-org-request (eq (type-of req) 'jupyter-org-request)))
      (when is-org-request
        (jupyter-with-message-content msg (status payload)
          (when (and (jupyter-org-request-async-p req)
                     (equal status "ok")
                     (not (jupyter-org-request-id-cleared-p req)))
            (jupyter-org--clear-request-id req)
            (org-with-point-at (jupyter-org-request-marker req)
              (org-babel-remove-result)))))
      args))

  (unless (advice-member-p #'/jupyter-remove-empty-async-results 'jupyter-handle-execute-reply)
    (advice-add 'jupyter-handle-execute-reply :filter-args #'/jupyter-remove-empty-async-results)))

MoritzMaxeiner avatar Jul 08 '23 23:07 MoritzMaxeiner

Brilliant, not sure how I overlooked that type - thanks!

timlod avatar Jul 10 '23 03:07 timlod