Monthly Archives: February 2014

Automatically Fixing Python flake8 and pylint Errors in Emacs

Having recently installed elpy in my emacs environment and enabled flake8, I was presented with many warnings an errors based on style. At first I just starting disabling them all to avoid the noise and hassle of fixing them. Then I got the idea that it shouldn’t be that hard to create a fixer system to automatically fix at least the most simple stylistic errors. This idea was inspired in part by the automatic fix-up stuff I found while checking out Pycharm.

To create this system I started out with elisp only, and then in an attempt to learn how to use pymacs incorprated that. In this post I’m going to focus on the elisp version.

By checking out the code to display the error on the current line from flymake I derived a pair of functions and an associative array to invoke functions given a certain error found. Then I wrote fixer functions and added them to the array. I also I bind “C-cC-\” to pyfixer:fix-current-line.

Here’s the code with a single fixer function defined. Eventually I plan on hosting this on github so that people may contribute fixers.

(defun pyfixer:add-blank-line (errno errinfo)
  "Add blank line above current line"
  (save-excursion
    (beginning-of-line)
    (newline)))

(setq pyfixer:flymake-fixers
      '(
        ("E301" . pyfixer:add-blank-line)
        ("E302" . pyfixer:add-blank-line)
        ))

(defun pyfixer:fix-error (errdata)
  "Fix the given errdata"
  (if errdata
      (let (errno fixer)
        ;; Handle flake8 or pylint messages
        (if (string-match "\\(^\\([EW][0-9]+\\) \\|\\[\\([EW][0-9]+\\)\\(-.*\\)\\?*\\]\\).*" errdata)
            (progn
              (if (not (setq errno (match-string 2 errdata)))
                  (setq errno (match-string 3 errdata)))
              (setq fixer (cdr (assoc errno pyfixer:flymake-fixers)))
              ;; If we don't have and elisp fixer check pymacs fixers
              (if (and (not fixer) (functionp 'pyfixers-get-fixer-func))
                  (setq fixer (pyfixers-get-fixer-func errno)))
              (if fixer
                  (progn
                    (funcall fixer errno errdata)
                    (flymake-start-syntax-check))
                (message "No pyfixer function for: %s" errno)))))))

(defun pyfixer:fix-current-line ()
  "Display a fix for the current line"
  (interactive)
  (let* ((line-no             (flymake-current-line-no))
         (line-err-info-list  (nth 0 (flymake-find-err-info flymake-err-info line-no)))
         (menu-data           (flymake-make-err-menu-data line-no line-err-info-list))
         (errlist (caadr menu-data)))
    (message "Errlist: %s" errlist)
    (mapcar 'pyfixer:fix-error errlist)))

(global-set-key "\C-c\C-\\" 'pyfixer:fix-current-line)

Now this fixer function is quite simple and doesn’t deal with some cases. For example if a comment immediately precedes the function definition it is not counted as a blank line, but the extra line will be between it and the function definition. To fix this we backup to the first non-comment line before adding any newlines. Additionally, let’s parse the error message to determine exactly how many newlines we need to add. Here’s the revised fixer function:

(defun line-no-commentp ()
  (save-match-data
    (let* ((start (line-beginning-position))
           (end (line-end-position))
           (line (buffer-substring-no-properties start end)))
      (not (string-match "[:space:]*#.*" line)))))

(defun pyfixer:add-blank-line (errno errinfo)
  "Add blank line above current line"
  (save-excursion
    (let ((lines 0))
      (if (string-match "expected \\([0-9]+\\) blank lines?, found \\([0-9]+\\)" errinfo)
          (setq lines (- (string-to-number (match-string 1 errinfo))
                         (string-to-number (match-string 2 errinfo)))))
      (beginning-of-line)
      (previous-line)
      (while (not (line-no-commentp))
        (previous-line))
      (next-line)
      (newline lines))))

Extracting an Environment Variable From a Process

Occasionally I find it necessary to extract the value of an environment variable of a running process. For example to examine the TMUX value of a given process. I’ll use this later to determine if I should signal an emacs process when a TMUX session is reattached. This function depends on /proc/<pid>/environ being supported (apparently a linux thing). Here’s the function.

[Ed: updated version with suggested replacement from KnowsBash reddit-bash comment]

get-pid-env-var () {
    if [ -z "$1" -o -z "$2" ]; then
        echo "usage: get-pid-env-var pid var"
        return 1
    fi
    local key value
    while IFS='=' read -rd '' key value; do
        if [[ $key = "$2" ]]; then
            printf '%s\n' "$value"
            break
        fi
    done < "/proc/$1/environ"
}

And here it is in use.

[22:36:28 ~]$ ps x | grep emacs
24414 pts/1    S+     0:04 emacs -nw .bashrc
26751 pts/4    S+     0:00 grep emacs
[22:36:32 ~]$ get-pid-env-var 24414 TMUX
/tmp/tmux-21995/default,24155,0

Here’s the old version.

get-pid-env-var () {
    if [ -z "$1" -o -z "$2" ]; then
        echo "usage: get-pid-env-var pid var"
        return 1
    fi
    local VAR=$(tr "\0" "\n" < /proc/$1/environ | grep "$2=")
    if [ -z "$VAR" ]; then
        return 1
    fi
    echo ${VAR#$2=}
    return 0
}

A Pymacs Code Example

This is a simple example of using pymacs to fix a flake8 warning. The warning is covering use of = or ! with None. The fixer is invoked with the point on the line containing the warning or error. This is part of a system I’m building to support auto-correction of flake8 and pylint warnings in emacs. As the system matures I plan to host it on github so that others may also contribute to it.

Here is the actual fixer function with a utility function to get the string of the current line. A key to understanding what’s going on with the code is that lisp.function(arg) is the equivalent of (function arg) in elisp.

def get_line_string ():
    """Get the start, end and string of the line the point is on."""
    start = lisp.line_beginning_position()
    end = lisp.line_end_position()
    return start, end, lisp.buffer_substring(start, end)

def fixer_equals_none (unused_error, unused_errtext):
    start, end, line = get_line_string()
    newline = re.sub(r"==\s*None", r"is None", line)
    newline = re.sub(r"!=\s*None", r"is not None", newline)
    if newline != line:
        lisp.delete_region(start, end)
        lisp.insert(newline)

Obviously this function is invoked from somewhere else with the error number and text (e.g., E711 and E711: error text). The following is the code that does that.

fixers = {
    # ...
    "E711": fixer_equals_none,
}

def flymake_fix_current_line ():
    lineno = lisp.flymake_current_line_no()
    errinfo = lisp.flymake_err_info
    errlist = lisp.nth(0, lisp.flymake_find_err_info(errinfo.value(), lineno))
    menudata = lisp.flymake_make_err_menu_data(lineno, errlist)
    did_something = False
    #             caadr (x ((e1 e2 ...))) -> (e1 e2 ...)
    for errtxt in menudata[1][0]:
        if not errtxt:
            break
        try:
            m = re.match(r"([EFW]\d+) .*", errtxt)
        except Exception as ex:
            continue
        if not m:
            print("nomatch on {}".format(errtxt))
            continue
        key = m.group(1)
        if key in fixers:
            did_something = True
            fixers[key](key, errtxt)
    if did_something:
        lisp.flymake_start_syntax_check()

interactions[flymake_fix_current_line] = ''

Finally we have the elisp code that binds a key to the dispatch function.

(pymacs-load "fixers" "fixers-")
(global-set-key "\C-c\C-\\" 'fixers-flymake-fix-current-line)

Hello, World

Welcome to my blog. I’ve decided to start a blog in order to have a place to post code snippets and other customizations that I’ve found useful. These will either be something I’ve personally created or collected from around the web.

I hope you find it useful.