Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/dzangfan/pardef.el

Extensible Python docstring generator for GNU Emacs
https://github.com/dzangfan/pardef.el

docstring emacs generator python

Last synced: 11 days ago
JSON representation

Extensible Python docstring generator for GNU Emacs

Awesome Lists containing this project

README

        

* pardef

A Python docstring generator, uses [[https://sphinx-rtd-tutorial.readthedocs.io/en/latest/docstrings.html][Sphinx docstring format]].

[[file:example/example.gif]]

** Installation

Install [[file:pardef.el][pardef.el]] and put following code in your ~init.el~ or ~.emacs~:

#+begin_src emacs-lisp
;; This package doesn't depend `python-mode', but load after python
;; can speed up your initialization time.
(with-eval-after-load 'python
(add-to-list 'load-path "path/to/your/pardef")
(require 'pardef)
(define-key python-mode-map (kbd "M-d M-d") #'pardef-sphinx)
(define-key python-mode-map (kbd "M-d M-n") #'pardef-do-jump-forward)
(define-key python-mode-map (kbd "M-d M-p") #'pardef-do-jump-backward)
(define-key python-mode-map (kbd "M-d M-f")
#'pardef-do-jump-forward-and-kill)
(define-key python-mode-map (kbd "M-d M-b")
#'pardef-do-jump-backward-and-kill)
(define-key python-mode-map (kbd "M-p")
#'python-nav-backward-defun)) ; See `python-mode'

;; Or `use-package'
(use-package pardef
:after python
:load-path "/path/2/your/pardef"
:bind (:map python-mode-map
("M-d M-d" . pardef-sphinx)
("M-d M-n" . pardef-do-jump-forward)
("M-d M-p" . pardef-do-jump-backward)
("M-d M-f" . pardef-do-jump-forward-and-kill)
("M-d M-b" . pardef-do-jump-backward-and-kill)
("M-p" . python-nav-backward-defun))) ; See `python-mode'
#+end_src

Note that this package depends [[https://github.com/magnars/dash.el][dash]], so ensure ~dash.el~ is in your ~load-path~.

** Usage

Moving your cursor(or ~point~, in Emacs term) to the line that contains ~def~ keyword, and call function ~pardef-sphinx~ either by command (~M-x~) or by key binding (~M-d M-d~, if you followed our install manual), then you can see a new docstring is generated if there is no docstring originally, or a error message if your function definition contains some feature do not support now, or something horrible if there has an existed docstring and conflicts with ~pardef~.

Furthermore, as the demo in the beginning, you can jump (or jump-and-kill) between placeholders in docstring. Placeholders has form ~[TAG]~, and ~TAG~ can be customized by ~pardef-do-jumpable-tags~. You can use them intuitively.

** Mechanism

The main command ~pardef~ provided is ~pardef-sphinx~, which can generate and update docstring for Python function. A typical (or ~pardef~ recognizable) docstring's structure is

#+begin_src python
def verify(pwd):
'''Summary... <- Summary Part

:param pwd: Password. <- Parameter List Part
...

Detail... <- Rest Part
'''
#+end_src

Parts are identified by blank line, so your summary part can contain multiline, but cannot contain blank line. Other blocks are identified similarly. In [[https://sphinx-rtd-tutorial.readthedocs.io/en/latest/docstrings.html][Sphinx format]], we always consider the first block as a summary and parse literally, and treat the second block as a parameter list (of course, includes return value, return type, exceptions, etc.) block, finally, blocks following these two are rest blocks, they can have number of blocks separated by blank line, and are interpreted literally.

It is only significant when ~pardef~ update docstring. ~pardef~ trusts that the second block is a parameter list, so if it's unable to parse the second one, it'll think that there is something wrong and insert a new parameter list instead of update it.

Parameter list part also has its structure, refer to [[https://sphinx-rtd-tutorial.readthedocs.io/en/latest/docstrings.html#the-sphinx-docstring-format][Sphinx format]]:

#+begin_src python
"""[Summary]

:param [ParamName]: [ParamDescription], defaults to [DefaultParamVal]
:type [ParamName]: [ParamType](, optional)
...
:raises [ErrorType]: [ErrorDescription]
...
:return: [ReturnDescription]
:rtype: [ReturnType]
"""
#+end_src

~pardef~ will move unrecognized tags (such as ~raises~) between ~param~ and ~return~ when invoke updating.

** Customization

*** ~pardef-docstring-style~: =​'''​= or =​"""​=

Use double or single quotes as docstring's bound to output. This value will not affect parsing existed docstring.

*** ~pardef-enable-class-docstring~: Boolean

Whether to inserting/updating constructor ~__init__~'s docstring to class's definition.

#+begin_src python
class CL:
# ^
# (setq pardef-enable-class-docstring t) ;; default

def __init__(self):
# ^
# (setq pardef-enable-class-docstring nil)
raise NotImplementedError()
#+end_src

*** ~pardef-sphinx-list-indent~: non-negative integer

Indentations of lines like ~:param x:~, defaults to zero.

#+begin_src python
def f(x):
'''[Summary]

:param x: [ParamDescription]
^
(setq pardef-sphinx-list-indent 0) ;; default
:param x: [ParamDescription]
^
(setq pardef-sphinx-list-indent 2)
'''
#+end_src

*** ~pardef-sphinx-add-defaults~: Boolean

Whether to generate a ~defaults to xx~ clause for parameter that has default value, defaults to ~t~.

#+begin_src python
def f(x=None):
'''[Summary]

:param x: [ParamDescription], defaults to None
^
(setq pardef-sphinx-add-defaults t)
:param x: [ParamDescription]
^
(setq pardef-sphinx-add-defaults nil)
'''
#+end_src

Note that this clause WONT be updated if you change the default later, so you need to update this clause manually.

*** ~pardef-sphinx-ignore-*~: Boolean

It means whether to ignore some parameter, and includes two variables:

- ~pardef-sphinx-ignore-rest~
Whether to ignore ~*args~, parameter can use arbitrary name, defaults to ~nil~
- ~pardef-sphinx-ignore-keyword~
Whether to ignore ~**args~, defaults to ~nil~

Furthermore, you can ignore parameter has arbitrary name you want, see below.

*** ~pardef-sphinx-ignored-parameters~: List of String

All parameter has name exactly matched any one in this list will be ignored. Its default value is ~self~ and ~cls~, but you can customize it to anything you like.

#+begin_src python
class C:
def __init__(self, p, q): ...
# ^
# ignore

@classmethod
def M(cls, t, s): ...
# ^
# ignore
#+end_src

However, if you provided a type annotation or a default value or both to parameter, then we consider that it may have some special feature and generate document for it even it's in ~pardef-sphinx-ignored-parameters~.

*** ~pardef-sphinx-default-*~: String

Default value for your docstring's fields. It contains three variable:

- ~pardef-sphinx-default-summary~: Defaults to ~[Summary]~
- ~pardef-sphinx-default-param~: Defaults to ~[ParamDescription]~
- ~pardef-sphinx-default-return~: Default to ~[ReturnDescription]~

They correspond with

#+begin_src python
'''

:param x:
:return:
'''
#+end_src

You may modify them carefully, since they are connecting with placeholder ~[TAG]~. See next section.

*** ~pardef-do-jumpable-tags~: List of String

Tags that can be searched by function ~pardef-do-jump-*[-and-kill]~, where \* is ~forward~ or ~backward~, You needn't to care them if you use default key binding mentioned in install section. It defaults to ~ParamDescription~, ~ReturnDescription~ and ~Summary~, so when you invoke ~pardef-do-jump-forward-and-kill~ (or ~M-d M-f~), character sequence ~[ParamDescription]~ may be going to suffer, but ~List[str]~ wont. You can define your own jump-tag by push new string into it, then ~[YourTag]~ will be significant as a placeholder for ~M-d M-f~.

Finally, do not define tag whose name likes ~str~, ~int~, and so on, otherwise character sequence like ~List[str]~ may suffer from friendly fire.

** Development

~pardef~ is extensible, you can write a plugin to define your own docstring format. A plugin is a function who take specified parameters and produce specified values, we call it ~renderer~. In particular, ~renderer~ has sign (pseudo-code):

#+begin_src python
def rerderer(defun: DefunSpecifier,
docstring: Optional[List[str]]) \
-> Tuple[List[str], int, int]: ...
#+end_src

where ~DefunSpecifier~ is a [[https://www.gnu.org/software/emacs/manual/html_node/elisp/Association-Lists.html][association list]] has fixed form:

- *name* Name of function (~string~)
- *params* Parameter list (~list~)
- *return* Return type (~string~)

All keys (e.g. *name*, *params*) are symbol. Furthermore, *params* corresponds to parameter list, which is a list of list and each elements has form

#+begin_src emacs-lisp
(list "parameter_name"
"parameter_type" ; May be empty
"parameter_default_value") ; May be empty
#+end_src

You can use function ~pardef-load-python-defun~ to load a string as a python function definition:

#+begin_src emacs-lisp
(pardef-load-python-defun "def F(x, y: int, z = 0) -> R:") ; Note the colon
;; RESULT:
;;
;; ((name . "F")
;; (params
;; ("x" "" "" )
;; ("y" "int" "" )
;; ("z" "" "0"))
;; (return . "R"))
#+end_src

This structure will be passed to your ~renderer~. The latter parameter indicates we need you create a new docstring or update a present docstring. It's ~nil~ if a new docstring is required, or a list of ~string~ which represents each line of a present docstring and you should return a new docstring base on it (note that the first element and the last element contains bound of docstring, both ~'''~ and ~"""~ are possible).

The you can do arbitrary funny computation on them (and only on them, do not modify the buffer) and return a docstring specifier, which is a 3-tuple (or ~list~, in Emacs Lisp) consists of

- *docstring* A list of string like the second parameter we just mentioned, but it shouldn't be ~nil~
- *row index* A non-negative integer indicates the cursor's position relative to the first line of new docstring, based on zero
- *column index* A integer like *row index*, indicates cursor's column

Both of *row index* and *column index* are relative position and you needn't consider the indentation. Finally, let's implement a simple ~renderer~:

#+begin_src emacs-lisp
(defun ultimate-renderer (_defun-definition origional-docstring)
(if (null origional-docstring)
;; Create a new docstring
(list (list (concat pardef-docstring-style ; Not ''' or """
"Amazing physics going on...")
pardef-docstring-style)
;; Move to the beginning of our new docstring
0 (length pardef-docstring-style))

;; If `origional-docstring' isn't `nil', it contains the present
;; docstring and is in the form of list of string, whose each
;; element represents a line of docstring (trimmed indentation).
(list (append (list (car origional-docstring)
"Successfully flipped one bit!")
(cdr origional-docstring))
;; Move to the end of our updated docstring
(length origional-docstring)
(length pardef-docstring-style))))
#+end_src

After that, you can pass this ~renderer~ to ~pardef-gen~, who is the major function to generate docstring. Note that ~pardef-gen~ is not ~interactive~, so we need to wrap our ~renderer~ with ~defun~ or ~lambda~:

#+begin_src emacs-lisp
(define-key python-mode-map (kbd "M-d M-d")
(lambda () (interactive) (pardef-gen #'ultimate-renderer)))
#+end_src

[[file:example/example-dev.gif]]

** Known Issues

~pardef~ assume that there is no more continuous line after the line that contains function definition terminate notation ~:~. Specifically, ~pardef~ will not generate a correct docstring for following code:

#+begin_src python
def verify(pwd): return \
pwd == "asd123456"

# But these are no problem
def verify(pwd): return pwd == "asd123456"

def verify(pwd) \
:return pwd == "asd123456"
#+end_src

In addition, we suggest that do not put your comment in some strange place, for example:

#+begin_src python
def War_and_Peace():
## Chapter 3 ##
'''[Summary]

:return:...
'''
#+end_src

~pardef~ will not work if you want to update this docstring: it will insert a new one instead of update the origin. Following code will work as you are thinking:

#+begin_src python
def War_and_Peace():
## Chapter 3 ##
return ...
#+end_src

~pardef~ will insert a new docstring between function definition and comment.

** License

GPL-3