Table of Contents

This program was inspired by this vim plugin which when I looked at the Vimscript source code made me wonder about how complex roughly equivalent elsip would look. It was an adventure in learning regexes and Emacs regexp foo.

It also is an exploration in applying the How to Design Programs methodology within org-mode using literate programming. There are some rough edges because the code will run in Emacs' significantly stateful context.

Overview

Purpose

The script sorts a collection of Python import statements alphabetically using the specific name imported. It ignores the from prefix of the statement.

Examples

Suppose we have the following Python Source

import m
import j
from a import n
from c import k
from b import i

def spam(a):
    return a

spam(3)

Running the utility should modify the source to

from b import i
import j
from c import k
import m
from a import n

def spam(a):
    return a

spam(3)

Template

  1. Find lines containing import statements
  2. Parse lines to find the name of the import
  3. Sort lines based on name of import
  4. Replace old lines with sorted lines
;;; Helpers

<<search-helpers>>

<<parse-helpers>>

<<sort-helpers>>

<<replace-helpers>>

;;; Main

(defun python-import-sort ()
  (interactive)
  (fundamental-mode)
  (save-excursion
    (first-matching-line import-match)
    (let ((place-to-insert (point-marker))
          (whole-buffer (buffer-string)))
      (with-temp-buffer
        (fundamental-mode)
        <<edit-temp-buffer>>
        (setq whole-buffer (buffer-string))); end with-temp-buffer
      (buffer-flush-matching-lines import-match)
      (goto-char place-to-insert)
      (insert whole-buffer))))

DONE Edit Temp Buffer

(insert whole-buffer)
(buffer-flush-not-matching-lines import-match)
(let* ((modules (parse-import-statements (buffer-to-list)))
       (sorted-modules (sort-python-modules modules)))
  (flush-lines "")
  (insert-python-import-statements sorted-modules))

DONE Search Helpers

A collection of functions to facillitate searching a buffer and operating upon the buffer based on search.

;;; Search Helper

<<import-match>>

<<buffer-flush-matching-lines>>

<<buffer-flush-not-matching-lines>>

<<first-matching-line>>

import-match

  • Purpose A constant for matching import statements. Here the value is a simple string but it could be a regexp.
  • Implementation

    ;;; The match string for flushing and keeping
    (setq import-match "import")
    

buffer-flush-matching-lines

  • Purpose Purge/flush all lines from a buffer that match a regexp.
  • Example

    Given a buffer containing.
    
      # Using spam
      import spam
      spam.eggs(4)
      print("eggs are tasty")
      spam.spam('spam')
    
    Calling buffer-flush-matching-lines("eggs") yields
    
      # a comment
      import spam
      spam.spam('spam')
    
  • Implementation

    (defun buffer-flush-matching-lines (regex)
      "Flushes matching lines from buffer."
      (mark-whole-buffer)
      (flush-lines regex))
    

buffer-flush-not-matching-lines

  • Purpose Purge/flush all lines from a buffer that do not match a regexp.
  • Example

    Given a buffer containing.
    
      # Using spam
      import spam
      spam.eggs(4)
      print("eggs are tasty")
      spam.spam('spam')
    
    Calling buffer-flush-not-matching-lines("eggs") yields
    
      spam.eggs(4)
      print("eggs are tasty")
    
  • Implementation

    (defun buffer-flush-not-matching-lines (regex)
      "Removes non-matching lines from buffer."
        (mark-whole-buffer)
        (keep-lines regex))
    

first-matching-line

  • Purpose Plase point at start of first line in buffer that contains match for regexp
  • Example

    Given a buffer containing.
    
      # Using spam
      import spam
      spam.eggs(4)
      print("eggs are tasty")
      spam.spam('spam')
    
    Calling first-matching-line("eggs") moves the point to the beginning of
    
      spam.eggs(4)
    
  • Implementation

    (defun first-matching-line (regex)
      "Finds the start of line for the first line matching regex."
      (beginning-of-buffer)
      (search-forward regex)
      (move-beginning-of-line nil))
    

DONE Parse Helpers

Story I spent a couple of hours (much longer than I expected) goofing around with elisp and regexp's to figure out a way to make string matching work. I am sure there is a simpler way.

The helper function parse-import-statement contains the final regex.

Purpose

To create a data structure upon which the lines can be sorted.

Data Structure

The data structure is a dotted list. The first element is the original line and the second element is the name of what is actually imported.

("from a import n" . "n")

Examples

(parse-import-statements
  '("import m"
    "import j"
    "from a import n"
    "from c import k"
    "from b import i"))
; result
'(("import m" . "m")
  ("import j" . "j")
  ("from a import n" . "n")
  ("from c import k" . "k")
  ("from b import i" . "i"))

Template

;;; Parse Helpers

<<buffer-to-list>>

<<parse-import-statement>>

<<parse-import-statements>>

buffer-to-list

  • Purpose

Convert a buffer to a list of strings based on lines.

  • Example Given the buffer:

    import m
    import j
    from a import n
    from c import k
    from b import i
    
    def spam(a):
        return a
    
    spam(3)
    

    buffer-to-list returns

    '("import m"
      "import j"
      "from a import n"
      "from c import k"
      "from b import i"
      "def spam(a):"
      "    return a"
      "spam(3)"))
    
  • Implementation

    (defun buffer-to-list ()
      "Converts a buffer to a list of lines."
      (split-string (buffer-string) "\n" t))
    

parse-import-statements

  • Purpose The high level parsing function.
  • Examples

    given
    
      (setq example '("import m"
                      "import j"
                      "from a import n"
                      "from c import k"
                      "from b import i"))
    then
    
      (parse-import-statements example)
    
    returns:
    
      (("from b import i" . "i")
       ("from c import k" . "k")
       ("from a import n" . "n")
       ("import j" . "j")
       ("import m" . "m"))
    
  • Implementation

    (defun parse-import-statements (statements &optional a-list)
      "Parses each statement in statements. Returns a list of (statement . sort-term)."
      (if (null statements)
          a-list
        (parse-import-statements
         (rest statements)
         (cons (parse-import-statement (first statements))
               a-list))))
    

parse-import-statement

  • Purpose Parse one import statement.
  • Examples

    Given
      (parse-import-statement "from a import n")
    Return
      ("from a import n" . "n")
    
  • Implementation

    (defun parse-import-statement (line)
      "Takes a Python import statement as a string.
    Returns a dotted list of: (import-statement . sort-term)."
      (let* ((matcher "\\(\\w+ import \\|import \\)\\(\\w\\)")
             (match (string-match matcher line)))
        (cons line (match-string 2 line))))
    

TODO provide flexibility in matching white space

Currently, only a single space between import and the name is matched.

DONE Sort Helpers

Data Structure

  • Purpose The data structure is a dotted list. The first element is the original line and the second element is the name of the python module to be imported. Because the data structure is a dotted list rather than a proper list, the second element cannot be accessed with second because second is implemented as (car(cdr alist)).
  • Example

    ("from a import n" . "n")
    

Template

;;; sort helpers

<<python-module-less-than>>

<<sort-python-modules>>

python-module-less-than

  • Purpose

Compare two data structures based on second element which is the name of the python module.

  • Examples

    (python-module-less-than '("from a import n" . "n") '("import m" . "m"))
    ; result is false
    
  • Implementation

    (defun python-module-less-than (lhs rhs)
      "Returns true if the module name of the left hand side is less than the module name of the right hand side."
      (string< (cdr lhs) (cdr rhs)))
    

sort-python-modules

  • Purpose Sort data structures based on second element which is the name of the module.
  • Examples

    (sort-python-modules '(("from a import n" . "n") '("import m" . "m")))
    ; return '(("import m" . "m") ("from a import n" . "n"))
    
  • Implementation

    ;;; sorting helper
    (defun sort-python-modules (structures)
      "Sorts data structures using data-structure-less-than"
      (sort structures #'python-module-less-than))
    

DONE Replace Helpers

;; replace helpers

<<insert-python-import-statement>>

<<insert-python-import-statements>>

insert-python-import-statement

  • Purpose Insert data structure string.
  • Examples

    (inster-python-import-statement '("from a import n" . "n"))
    ; inserts "from a import n" in current buffer
    
  • Implementation

    (defun insert-python-import-statement (ds)
      "Inserts the string (car) of a data structure into current buffer"
      (insert (car ds))
      (newline))
    

insert-python-import-statements

  • Purpose Insert strings from all data structures. Uses mapc instead of mapcar because it is called for side-effects.
  • Examples

    (insert-python-import-statements '(("import m" . "m") ("from a import n" . "n")))
    ; inserts "import m
    ;          from a import n" 
    ; in current buffer.
    
  • Implementation

    (defun insert-python-import-statements (structures)
      "Inserts the ordered statements into current buffer."
      (mapc #'insert-python-import-statement structures))
    

Notes

Emacs

Some useful emacs commands including

  1. M-x keep-lines
  2. M-x flush-lines

Some useful elisp:

  1. string-match to 'compile' a regex against a string.
  2. match-string to extract the groups of a match. These are used in the parse-import-statement.

HTDP

The template for writing code using the HTDP recipes is recursive. From the overall high level program down toward each function we have:

  1. Purpose
  2. Examples
  3. Template
  4. Tests (hopefully)
  5. Implementation (sometimes)
  6. Helpers

In the literate programming model the template may be mostly a noweb construct. The helpers are there as a short circuit of the recursive structure.

  • Implementation Using unordered lists as the structure for HTDP elements makes their orginization constant regardless of the outline level within org-mode.

TODO write elisp code to create an HTDP outline from the template.

Author: benrudgers

Created: 2017-05-10 Wed 16:44

Validate