Ob-ipython Enhancement (completion eldoc help)


tags: emacs, python

Introduction

ob-ipython is "an Emacs library that allows Org mode to evaluate code blocks using a Jupyter kernel (Python by default)."

It's a great tool for you to make note and literal programming. It's easy to use comparing with ein. And you can make a matplot inline image in the org file comparing with ob-python.

Problem

However it's not working properly when using a virtual environment.

When you don't have a system level jupyter installed and use org-mode, you will get a strange error like below.

/img/ob-ipython-error.gif

It can be fixed easily. When there is no jupyter installed, we just don't configure the kernel. When the time we use a virtual environment, and there will be a jupyter installed!

  (advice-add 'ob-ipython-auto-configure-kernels :around
              (lambda (orig-fun &rest args)
                "Configure the kernels when found jupyter."
                (when (executable-find ob-ipython-command)
                  (apply orig-fun args))))

/img/ob-ipython-error-fix.gif

Notice: We will using a virtualenv environment called scientific, which has numpy and jupyter installed.

Completion

The company backend in ob-ipython is extremely slow, which means it basic can't complete anything.

ob-ipython will start a backend process ob-ipython-kernel, sending the code to the process, and get the result. You can't have any interaction with the process for preventing you modify the "environment". But sometimes it's not convenient, we want to test some function. So we can start a inferior-python process, which is the process when we call run-python. When we evaluate our code using C-c C-c we also send the code to the process we started. Now we can even use the inferior-python process to complete the code!

  (defun run-python-first (&rest args)
    "Start a inferior python if there isn't one."
    (or (comint-check-proc "*Python*") (run-python)))

  (advice-add 'org-babel-execute:ipython :after
              (lambda (body params)
                "Send body to `inferior-python'."
                (run-python-first)
                (python-shell-send-string body)))

/img/ob-ipython-completion.gif

  (add-hook 'org-mode-hook
            (lambda ()
              (setq-local completion-at-point-functions
                          '(pcomplete-completions-at-point python-completion-at-point))))

We configure the completion-at-point-function, now you can use any completion backend you like.

However, we need to evaluate the code very ofen, which will send the code to a ob-ipython-kernel to display the result, and meantime send the code to a inferior-python process to get the code completion done.

Eldoc

Now we have an inferior-python process for testing our code and code completion. It also can be used to display the eldoc, too!

  (defun ob-ipython-eldoc-function ()
    (when (org-babel-where-is-src-block-head)
      (python-eldoc-function)))

  (add-hook 'org-mode-hook
            (lambda ()
              (setq-default eldoc-documentation-function 'ob-ipython-eldoc-function)))

/img/ob-ipython-eldoc.gif

Help

Sometimes we want a full description of the package or function.

Here's the idea: we send our code to the ob-ipython-kernel to get the result, we can also send help(package or function name) (e.g. help(os.path.join)) to the ob-ipython-kernel to get the help information, and display it in a separate buffer, just like what we type in a inferior-python process!

/img/ob-ipython-help.gif

  (defun ob-ipython-help (symbol)
    (interactive (list (read-string "Symbol: " (python-eldoc--get-symbol-at-point))))
    (unless (org-babel-where-is-src-block-head)
      (error "Symbol is not in src block."))
    (unless (ob-ipython--get-kernel-processes)
      (error "There is no ob-ipython-kernal running."))
    (when-let* ((processes  (ob-ipython--get-kernel-processes))
                (session (caar processes))
                (ret (ob-ipython--eval
                      (ob-ipython--execute-request (format "help(%s)" symbol) session))))
      (let ((result (cdr (assoc :result ret)))
            (output (cdr (assoc :output ret))))
        (let ((buf (get-buffer-create "*ob-ipython-doc*")))
          (with-current-buffer buf
            (let ((inhibit-read-only t))
              (erase-buffer)
              (insert output)
              (goto-char (point-min))
              (read-only-mode t)
              (pop-to-buffer buf)))))))

Conclusion

We evaluate our code and get the full doc using the ob-ipython-kernel.

We start a inferior-python process to test out code, get the completion and eldoc done.

You can get all the code here. Thanks for your time.