456 lines
17 KiB
EmacsLisp
456 lines
17 KiB
EmacsLisp
![]() |
;;; 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
|