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
- Find lines containing import statements
- Parse lines to find the name of the import
- Sort lines based on name of import
- 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
becausesecond
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 ofmapcar
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
M-x keep-lines
M-x flush-lines
Some useful elisp:
string-match
to 'compile' a regex against a string.match-string
to extract the groups of a match. These are used in theparse-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:
- Purpose
- Examples
- Template
- Tests (hopefully)
- Implementation (sometimes)
- 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
.