Category Archives: Python

Useful for python development

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))))

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)