add lisp packages
This commit is contained in:
455
lisp/sphinx-doc.el
Normal file
455
lisp/sphinx-doc.el
Normal file
@@ -0,0 +1,455 @@
|
||||
;;; sphinx-doc.el --- Sphinx friendly docstrings for Python functions
|
||||
|
||||
;; Copyright (c) 2013 <naikvin@gmail.com>
|
||||
|
||||
;; Author: Vineet Naik <naikvin@gmail.com>
|
||||
;; URL: https://github.com/naiquevin/sphinx-doc.el
|
||||
;; Package-Version: 20160116.1117
|
||||
;; Version: 0.3.0
|
||||
;; Keywords: Sphinx, Python
|
||||
;; Package-Requires: ((s "1.9.0") (cl-lib "0.5") (dash "2.10.0"))
|
||||
|
||||
;; This program is *not* a part of emacs and is provided under the MIT
|
||||
;; License (MIT) <http://opensource.org/licenses/MIT>
|
||||
;;
|
||||
;; Copyright (c) 2013 <naikvin@gmail.com>
|
||||
;;
|
||||
;; Permission is hereby granted, free of charge, to any person
|
||||
;; obtaining a copy of this software and associated documentation
|
||||
;; files (the "Software"), to deal in the Software without
|
||||
;; restriction, including without limitation the rights to use, copy,
|
||||
;; modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
;; of the Software, and to permit persons to whom the Software is
|
||||
;; furnished to do so, subject to the following conditions:
|
||||
;;
|
||||
;; The above copyright notice and this permission notice shall be
|
||||
;; included in all copies or substantial portions of the Software.
|
||||
;;
|
||||
;; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
;; EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
;; MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
;; NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
||||
;; BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
||||
;; ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
;; CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
;; SOFTWARE.
|
||||
|
||||
;;; Commentary:
|
||||
;;
|
||||
;; This file provides a minor mode for inserting docstring skeleton
|
||||
;; for Python functions and methods. The structure of the docstring is
|
||||
;; as per the requirements of the Sphinx documentation generator
|
||||
;; <http://sphinx-doc.org/index.html>
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'cl-lib)
|
||||
(require 'dash)
|
||||
(require 's)
|
||||
|
||||
|
||||
(defun sphinx-doc-current-line ()
|
||||
"Return current line as string."
|
||||
(buffer-substring-no-properties (point-at-bol) (point-at-eol)))
|
||||
|
||||
|
||||
;; regular expression to identify a valid function definition in
|
||||
;; python and match it's name and arguments
|
||||
(defconst sphinx-doc-fun-regex "^ *def \\([a-zA-Z0-9_]+\\)(\\(\\(?:.\\|\n\\)*\\)):$")
|
||||
|
||||
;; regexes for beginning and end of python function definitions
|
||||
(defconst sphinx-doc-fun-beg-regex "def")
|
||||
(defconst sphinx-doc-fun-end-regex ":\\(?:\n\\)?")
|
||||
|
||||
;; Variations for some field keys recognized by Sphinx
|
||||
(defconst sphinx-doc-param-variants '("param" "parameter" "arg" "argument"
|
||||
"key" "keyword"))
|
||||
(defconst sphinx-doc-raises-variants '("raises" "raise" "except" "exception"))
|
||||
(defconst sphinx-doc-returns-variants '("returns" "return"))
|
||||
|
||||
(defvar sphinx-doc-python-indent)
|
||||
|
||||
;; struct definitions
|
||||
|
||||
(cl-defstruct sphinx-doc-arg
|
||||
name ; name of the arg
|
||||
default) ; optional default value if specified
|
||||
|
||||
|
||||
(cl-defstruct sphinx-doc-fndef
|
||||
name ; name of the function
|
||||
args) ; list of arg objects
|
||||
|
||||
|
||||
(cl-defstruct sphinx-doc-field
|
||||
key ; one of the allowed field name keyword
|
||||
type ; optional datatype
|
||||
arg ; optional argument
|
||||
(desc "")) ; description
|
||||
|
||||
;; Note about various types of reST fields recognized by Sphinx and
|
||||
;; how they are represented using the `sphinx-doc-field` struct
|
||||
;; above. The `key` should be non-nil in all since that's how they are
|
||||
;; identified:
|
||||
;;
|
||||
;; 1. param: All params must have a valid `arg` whereas `type` is
|
||||
;; optional and `desc` will initially be an empty string
|
||||
;; 2. type: Must have valid `arg`
|
||||
;; 3. rtype: Must NOT have `type` or `arg`
|
||||
;; 4. returns: Must NOT have `type` or `arg`
|
||||
;; 5. raises: Must have a valid `arg`
|
||||
;;
|
||||
;; See Also: http://sphinx-doc.org/domains.html#info-field-lists
|
||||
|
||||
|
||||
(cl-defstruct sphinx-doc-doc
|
||||
(summary "FIXME! briefly describe function") ; summary line that fits on the first line
|
||||
before-fields ; list of comments before fields
|
||||
after-fields ; list of comments after fields
|
||||
fields) ; list of field objects
|
||||
|
||||
|
||||
(defun sphinx-doc-str->arg (s)
|
||||
"Build an arg object from string S."
|
||||
(let ((parts (mapcar #'s-trim (split-string s "="))))
|
||||
(if (cdr parts)
|
||||
(make-sphinx-doc-arg :name (car parts)
|
||||
:default (cadr parts))
|
||||
(make-sphinx-doc-arg :name (car parts)))))
|
||||
|
||||
|
||||
(defun sphinx-doc-fndef->doc (f)
|
||||
"Build a doc object solely from fndef F."
|
||||
(make-sphinx-doc-doc
|
||||
:fields (append
|
||||
(mapcar (lambda (a)
|
||||
(make-sphinx-doc-field
|
||||
:key "param"
|
||||
:arg (sphinx-doc-arg-name a)))
|
||||
(sphinx-doc-fndef-args f))
|
||||
(list (make-sphinx-doc-field :key "returns")
|
||||
(make-sphinx-doc-field :key "rtype")))))
|
||||
|
||||
|
||||
(defun sphinx-doc-fun-args (argstrs)
|
||||
"Extract list of arg objects from string ARGSTRS.
|
||||
ARGSTRS is the string representing function definition in Python.
|
||||
Note that the arguments self, *args and **kwargs are ignored."
|
||||
(when (not (string= argstrs ""))
|
||||
(mapcar #'sphinx-doc-str->arg
|
||||
(-filter
|
||||
(lambda (str)
|
||||
(and (not (string= (substring str 0 1) "*"))
|
||||
(not (string= str "self"))))
|
||||
(mapcar #'s-trim
|
||||
(split-string argstrs ","))))))
|
||||
|
||||
|
||||
(defun sphinx-doc-str->fndef (s)
|
||||
"Build a fndef object from string S.
|
||||
S is a string representation of the python function definition
|
||||
Returns nil if string is not a function definition."
|
||||
(when (string-match sphinx-doc-fun-regex s)
|
||||
(make-sphinx-doc-fndef
|
||||
:name (match-string 1 s)
|
||||
:args (sphinx-doc-fun-args (match-string 2 s)))))
|
||||
|
||||
|
||||
(defun sphinx-doc-field->str (f)
|
||||
"Convert a field object F to it's string representation."
|
||||
(cond ((and (stringp (sphinx-doc-field-arg f))
|
||||
(stringp (sphinx-doc-field-type f)))
|
||||
(s-format ":${key} ${type} ${arg}: ${desc}"
|
||||
'aget
|
||||
`(("key" . ,(sphinx-doc-field-key f))
|
||||
("type" . ,(sphinx-doc-field-type f))
|
||||
("arg" . ,(sphinx-doc-field-arg f))
|
||||
("desc" . ,(sphinx-doc-field-desc f)))))
|
||||
((stringp (sphinx-doc-field-arg f))
|
||||
(s-format ":${key} ${arg}: ${desc}"
|
||||
'aget
|
||||
`(("key" . ,(sphinx-doc-field-key f))
|
||||
("arg" . ,(sphinx-doc-field-arg f))
|
||||
("desc" . ,(sphinx-doc-field-desc f)))))
|
||||
(t (s-format ":${key}: ${desc}"
|
||||
'aget
|
||||
`(("key" . ,(sphinx-doc-field-key f))
|
||||
("desc" . ,(sphinx-doc-field-desc f)))))))
|
||||
|
||||
|
||||
(defun sphinx-doc-doc->str (ds)
|
||||
"Convert a doc object DS into string representation."
|
||||
(s-join
|
||||
"\n"
|
||||
(-filter
|
||||
(lambda (x) (not (equal x nil)))
|
||||
(list (s-format "\"\"\"$0\n" 'elt (list (sphinx-doc-doc-summary ds)))
|
||||
(when (and (sphinx-doc-doc-before-fields ds)
|
||||
(not (string= (sphinx-doc-doc-before-fields ds) "")))
|
||||
(concat (sphinx-doc-doc-before-fields ds) "\n"))
|
||||
(s-join "\n" (mapcar #'sphinx-doc-field->str
|
||||
(sphinx-doc-doc-fields ds)))
|
||||
""
|
||||
(when (and (sphinx-doc-doc-after-fields ds)
|
||||
(not (string= (sphinx-doc-doc-after-fields ds) "")))
|
||||
(concat (sphinx-doc-doc-after-fields ds) "\n"))
|
||||
"\"\"\""))))
|
||||
|
||||
|
||||
(defun sphinx-doc-parse (docstr indent)
|
||||
"Parse docstring DOCSTR into it's equivalent doc object.
|
||||
INDENT is the current indentation level of the Python function."
|
||||
(let* ((lines (mapcar (lambda (line)
|
||||
(s-chop-prefix (make-string indent 32) line))
|
||||
(split-string docstr "\n")))
|
||||
(paras (sphinx-doc-lines->paras lines))
|
||||
(field-para? #'(lambda (p) (s-starts-with? ":" (car p))))
|
||||
(comment? #'(lambda (p) (not (funcall field-para? p)))))
|
||||
(progn
|
||||
(make-sphinx-doc-doc
|
||||
:summary (caar paras)
|
||||
:before-fields (sphinx-doc-paras->str
|
||||
(-take-while comment? (cdr paras)))
|
||||
:after-fields (sphinx-doc-paras->str
|
||||
(cdr (-drop-while comment? (cdr paras))))
|
||||
:fields (sphinx-doc-parse-fields
|
||||
(car (-filter field-para? paras)))))))
|
||||
|
||||
|
||||
(defun sphinx-doc-paras->str (paras)
|
||||
"Convert PARAS to string.
|
||||
PARAS are list of paragraphs (which in turn are list of lines).
|
||||
This is done by adding a newline between two lines of each para
|
||||
and a blank line between each para."
|
||||
(s-join
|
||||
""
|
||||
(apply #'append
|
||||
(-interpose '("\n\n")
|
||||
(mapcar (lambda (p)
|
||||
(-interpose "\n" p))
|
||||
paras)))))
|
||||
|
||||
|
||||
(defun sphinx-doc-lines->paras (lines)
|
||||
"Group LINES which are list of strings into paragraphs."
|
||||
(reverse
|
||||
(mapcar
|
||||
#'reverse
|
||||
(car
|
||||
(cl-reduce (lambda (acc x)
|
||||
(let ((paras (car acc))
|
||||
(prev-blank? (cdr acc)))
|
||||
(cond ((string= x "") (cons paras t))
|
||||
(prev-blank? (cons (cons (list x) paras) nil))
|
||||
(t (cons (cons (cons x (car paras)) (cdr paras)) nil)))))
|
||||
(cdr lines)
|
||||
:initial-value (cons (list (list (car lines))) nil))))))
|
||||
|
||||
|
||||
(defun sphinx-doc-str->field (s)
|
||||
"Parse a single field into field object.
|
||||
Argument S represents a single field in the fields paragraph of
|
||||
the docstring."
|
||||
(cond ((string-match "^:\\([a-z]+\\) \\([a-z.]+\\) \\([a-zA-Z0-9_]+\\):\s?\\(.*\\(?:\n\s*.*\\)*\\)$" s)
|
||||
(make-sphinx-doc-field :key (match-string 1 s)
|
||||
:type (match-string 2 s)
|
||||
:arg (match-string 3 s)
|
||||
:desc (match-string 4 s)))
|
||||
((string-match "^:\\([a-z]+\\) \\([a-zA-Z0-9_]+\\):\s?\\(.*\\(?:\n\s*.*\\)*\\)$" s)
|
||||
(make-sphinx-doc-field :key (match-string 1 s)
|
||||
:arg (match-string 2 s)
|
||||
:desc (match-string 3 s)))
|
||||
((string-match "^:\\([a-z]+\\):\s?\\(.*\\(?:\n\s*.*\\)*\\)$" s)
|
||||
(make-sphinx-doc-field :key (match-string 1 s)
|
||||
:desc (match-string 2 s)))))
|
||||
|
||||
|
||||
(defun sphinx-doc-parse-fields (fields-para)
|
||||
"Parse FIELDS-PARA into list of field objects.
|
||||
FIELDS-PARA is the fields section of the docstring."
|
||||
(when fields-para
|
||||
(mapcar #'sphinx-doc-str->field
|
||||
(mapcar (lambda (s)
|
||||
(if (s-starts-with? ":" s) s (concat ":" s)))
|
||||
(split-string (s-join "\n" fields-para) "\n:")))))
|
||||
|
||||
|
||||
(defun sphinx-doc-merge-docs (old new)
|
||||
"Merge OLD and NEW doc objects.
|
||||
Effectively, only the fields field of new doc are merged whereas
|
||||
the remaining fields of the old object stay as they are."
|
||||
(make-sphinx-doc-doc
|
||||
:summary (sphinx-doc-doc-summary old)
|
||||
:before-fields (sphinx-doc-doc-before-fields old)
|
||||
:after-fields (sphinx-doc-doc-after-fields old)
|
||||
:fields (sphinx-doc-merge-fields
|
||||
(sphinx-doc-doc-fields old)
|
||||
(sphinx-doc-doc-fields new))))
|
||||
|
||||
|
||||
(defun sphinx-doc-select-fields (keys fields)
|
||||
"Return subset of fields with select keys.
|
||||
KEYS is a list of strings and FIELDS is a list of field objects."
|
||||
(-filter (lambda (f)
|
||||
(member (sphinx-doc-field-key f) keys))
|
||||
fields))
|
||||
|
||||
|
||||
(defun sphinx-doc-field-map (fields)
|
||||
"Create a mapping of field arg with the field for all FIELDS."
|
||||
(mapcar (lambda (f) (cons (sphinx-doc-field-arg f) f)) fields))
|
||||
|
||||
|
||||
(defun sphinx-doc-field-map-get (key mapping)
|
||||
"Return the value in the field mapping for the key or nil.
|
||||
KEY is a string and MAPPING is an associative list."
|
||||
(cdr (assoc key mapping)))
|
||||
|
||||
|
||||
(defun sphinx-doc-merge-fields (old new)
|
||||
"Merge old and new fields together.
|
||||
OLD is the list of old field objects, NEW is the list of new
|
||||
field objects."
|
||||
(let ((param-map (sphinx-doc-field-map
|
||||
(sphinx-doc-select-fields sphinx-doc-param-variants old)))
|
||||
(type-map (sphinx-doc-field-map
|
||||
(sphinx-doc-select-fields '("type") old)))
|
||||
(fixed-fields (sphinx-doc-select-fields
|
||||
(cons "rtype" (append sphinx-doc-returns-variants
|
||||
sphinx-doc-raises-variants))
|
||||
old)))
|
||||
(append (-mapcat
|
||||
(lambda (f)
|
||||
(let* ((key (sphinx-doc-field-arg f))
|
||||
(param (sphinx-doc-field-map-get key param-map))
|
||||
(type (sphinx-doc-field-map-get key type-map)))
|
||||
(cond ((and param type) (list param type))
|
||||
(param (list param))
|
||||
(t (list f)))))
|
||||
(sphinx-doc-select-fields sphinx-doc-param-variants new))
|
||||
fixed-fields)))
|
||||
|
||||
|
||||
;; Note: Following few functions (those using `save-excursion`) must
|
||||
;; be invoked only when the cursor is on the function definition line.
|
||||
|
||||
(defun sphinx-doc-get-region (srch-beg srch-end)
|
||||
"Return the beginning and end points of a region by searching.
|
||||
SRCH-BEG and SRCH-END are the chars to search for."
|
||||
(save-excursion
|
||||
(search-forward-regexp srch-beg)
|
||||
(let ((beg (point)))
|
||||
(search-forward-regexp srch-end)
|
||||
(vector beg (point)))))
|
||||
|
||||
|
||||
(defun sphinx-doc-current-indent ()
|
||||
"Return the indentation level of the current line.
|
||||
ie. by how many number of spaces the current line is indented"
|
||||
(save-excursion
|
||||
(let ((bti (progn (back-to-indentation) (point)))
|
||||
(bol (progn (beginning-of-line) (point))))
|
||||
(- bti bol))))
|
||||
|
||||
|
||||
(defun sphinx-doc-fndef-str ()
|
||||
"Return the Python function definition as a string."
|
||||
(save-excursion
|
||||
(let ((ps (sphinx-doc-get-region sphinx-doc-fun-beg-regex
|
||||
sphinx-doc-fun-end-regex)))
|
||||
(buffer-substring-no-properties (- (elt ps 0) 3) (- (elt ps 1) 1)))))
|
||||
|
||||
|
||||
(defun sphinx-doc-exists? ()
|
||||
"Return whether the docstring already exists for a function."
|
||||
(save-excursion
|
||||
(search-forward-regexp sphinx-doc-fun-end-regex)
|
||||
(s-starts-with? "\"\"\"" (s-trim (sphinx-doc-current-line)))))
|
||||
|
||||
|
||||
(defun sphinx-doc-existing ()
|
||||
"Return docstring of the function if it exists else nil."
|
||||
(when (sphinx-doc-exists?)
|
||||
(let* ((ps (sphinx-doc-get-region "\"\"\"" "\"\"\""))
|
||||
(docstr (buffer-substring-no-properties (elt ps 0)
|
||||
(- (elt ps 1) 3)))
|
||||
(indent (save-excursion
|
||||
(search-forward-regexp sphinx-doc-fun-end-regex)
|
||||
(sphinx-doc-current-indent))))
|
||||
(sphinx-doc-parse docstr indent))))
|
||||
|
||||
|
||||
(defun sphinx-doc-kill-old-doc (indent)
|
||||
"Kill the old docstring for the current Python function.
|
||||
INDENT is an integer representing the number of spaces the
|
||||
function body is indented from the beginning of the line"
|
||||
(save-excursion
|
||||
(let ((ps (sphinx-doc-get-region "\"\"\"" "\"\"\"\\(?:\n\\)?")))
|
||||
(kill-region (- (elt ps 0) 3) (+ (elt ps 1) indent)))))
|
||||
|
||||
|
||||
(defun sphinx-doc-insert-doc (doc)
|
||||
"Insert the DOC as string for the current Python function."
|
||||
(save-excursion
|
||||
(search-forward-regexp sphinx-doc-fun-end-regex)
|
||||
(forward-line -1)
|
||||
(move-end-of-line nil)
|
||||
(newline-and-indent)
|
||||
(insert (sphinx-doc-doc->str doc))))
|
||||
|
||||
|
||||
(defun sphinx-doc-indent-doc (indent)
|
||||
"Indent docstring for the current function.
|
||||
INDENT is the level of indentation"
|
||||
(save-excursion
|
||||
(let ((ps (sphinx-doc-get-region "\"\"\"" "\"\"\"")))
|
||||
(indent-rigidly (elt ps 0) (elt ps 1) indent))))
|
||||
|
||||
|
||||
(defun sphinx-doc ()
|
||||
"Insert docstring for the Python function definition at point.
|
||||
This is an interactive function and the docstring generated is as
|
||||
per the requirement of Sphinx documentation generator."
|
||||
(interactive)
|
||||
(if (string= (thing-at-point 'word) "def")
|
||||
(back-to-indentation)
|
||||
(search-backward-regexp sphinx-doc-fun-beg-regex))
|
||||
(let ((fd (sphinx-doc-str->fndef (sphinx-doc-fndef-str))))
|
||||
(if fd
|
||||
(let ((indent (+ (sphinx-doc-current-indent) sphinx-doc-python-indent))
|
||||
(old-ds (sphinx-doc-existing))
|
||||
(new-ds (sphinx-doc-fndef->doc fd)))
|
||||
(progn
|
||||
(when old-ds (sphinx-doc-kill-old-doc indent))
|
||||
(sphinx-doc-insert-doc
|
||||
(if old-ds
|
||||
(sphinx-doc-merge-docs old-ds new-ds)
|
||||
new-ds))
|
||||
(sphinx-doc-indent-doc indent)
|
||||
(search-forward "\"\"\""))))))
|
||||
|
||||
|
||||
(defvar sphinx-doc-mode-map
|
||||
(let ((m (make-sparse-keymap)))
|
||||
(define-key m (kbd "C-c M-d") 'sphinx-doc)
|
||||
m))
|
||||
|
||||
|
||||
;;;###autoload
|
||||
(define-minor-mode sphinx-doc-mode
|
||||
"Sphinx friendly docstring generation for Python code."
|
||||
:init-value nil
|
||||
:lighter " Spnxd"
|
||||
:keymap sphinx-doc-mode-map
|
||||
(when sphinx-doc-mode ; ON
|
||||
(set (make-local-variable 'sphinx-doc-python-indent)
|
||||
(cond ((boundp 'python-indent-offset)
|
||||
python-indent-offset)
|
||||
((boundp 'python-indent)
|
||||
python-indent)
|
||||
(t 4)))))
|
||||
|
||||
|
||||
(provide 'sphinx-doc)
|
||||
|
||||
;;; sphinx-doc.el ends here
|
||||
Reference in New Issue
Block a user