Add docformatter which formats Python docstrings to PEP 257 (#267)

Add [docformatter](https://github.com/PyCQA/docformatter) for Python
docstrings.

By default it outputs diffs but changes in-place with `--in-place`. On
successful change it exits with an error code of `3` (found out by
trial), so I had to add a formatter wrapping-script.

Initially I used `--in-place` with the special `in-place` symbol in
apheleia. But now I tried an approach where I transform the diff into
usable stdout using `patch` instead.

Related to #266 , where I had used the example of docformatter to ask
how to add scripts with positive exit codes and @raxod502 showed me the
`phpcs` solution.

---------

Co-authored-by: Radon Rosborough <radon@intuitiveexplanations.com>
This commit is contained in:
Michael Eliachevitch 2023-12-15 03:46:12 +01:00 committed by GitHub
parent 53c0389b5e
commit 4a87523f80
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 119 additions and 61 deletions

View file

@ -24,6 +24,7 @@ The format is based on [Keep a Changelog].
([#263]). ([#263]).
* [denofmt](https://docs.deno.com/runtime/manual/tools/formatter) for * [denofmt](https://docs.deno.com/runtime/manual/tools/formatter) for
js, jsx, ts, tsx, json, jsonc, md files. ([#264]) js, jsx, ts, tsx, json, jsonc, md files. ([#264])
* [docformatter](https://github.com/PyCQA/docformatter) for Python docstrings ([#267])
* [cljfmt](https://github.com/weavejester/cljfmt) for clojure, * [cljfmt](https://github.com/weavejester/cljfmt) for clojure,
clojurescript, edn files. ([#271]) clojurescript, edn files. ([#271])
@ -34,6 +35,7 @@ The format is based on [Keep a Changelog].
[#261]: https://github.com/radian-software/apheleia/pull/261 [#261]: https://github.com/radian-software/apheleia/pull/261
[#263]: https://github.com/radian-software/apheleia/pull/263 [#263]: https://github.com/radian-software/apheleia/pull/263
[#264]: https://github.com/radian-software/apheleia/pull/264 [#264]: https://github.com/radian-software/apheleia/pull/264
[#267]: https://github.com/radian-software/apheleia/pull/267
[#271]: https://github.com/radian-software/apheleia/pull/271 [#271]: https://github.com/radian-software/apheleia/pull/271
## 4.0 (released 2023-11-23) ## 4.0 (released 2023-11-23)

View file

@ -56,6 +56,7 @@
(denofmt-md . ("deno" "fmt" "-" "--ext" "md")) (denofmt-md . ("deno" "fmt" "-" "--ext" "md"))
(denofmt-ts . ("deno" "fmt" "-" "--ext" "ts")) (denofmt-ts . ("deno" "fmt" "-" "--ext" "ts"))
(denofmt-tsx . ("deno" "fmt" "-" "--ext" "tsx")) (denofmt-tsx . ("deno" "fmt" "-" "--ext" "tsx"))
(docformatter . ("apheleia-docformatter" inplace))
(dprint . ("dprint" "fmt" "--stdin" filepath)) (dprint . ("dprint" "fmt" "--stdin" filepath))
(elm-format . ("elm-format" "--yes" "--stdin")) (elm-format . ("elm-format" "--yes" "--stdin"))
(fish-indent . ("fish_indent")) (fish-indent . ("fish_indent"))

View file

@ -0,0 +1,5 @@
#!/bin/sh
docformatter --in-place "$@"
if [ "$?" -eq 3 ]; then
exit 0
fi

View file

@ -260,70 +260,63 @@ involve running any formatters."
Interactively, select a single formatter to test using Interactively, select a single formatter to test using
`completing-read'. If FORMATTERS is not provided (or, `completing-read'. If FORMATTERS is not provided (or,
interactively, with prefix argument), fall back to the FORMATTERS interactively, with prefix argument), fall back to the FORMATTERS
environment variable, defaulting to all formatters." environment variable, defaulting to all formatters.
This takes care of creating temporary file(s), if necessary for
the provided formatter, for example if `input' or `inplace' is
used, and substituting them in the command line. You can get the
name of the file used for input, if any, as a property on the
returned context."
(interactive (interactive
(unless (or current-prefix-arg noninteractive) (unless (or current-prefix-arg noninteractive)
(list (completing-read "Formatter: " (apheleia-ft--get-formatters))))) (list (completing-read "Formatter: " (apheleia-ft--get-formatters)))))
(setq-default indent-tabs-mode nil) (setq-default indent-tabs-mode nil)
(dolist (formatter (or formatters (apheleia-ft--get-formatters))) (dolist (formatter (or formatters (apheleia-ft--get-formatters)))
(dolist (in-file (apheleia-ft--input-files formatter)) (dolist (in-file (apheleia-ft--input-files formatter))
(let ((extension (file-name-extension in-file)) (let* ((extension (file-name-extension in-file))
(in-text (apheleia-ft--read-file in-file)) (in-text (apheleia-ft--read-file in-file))
;; The `in-temp-real-file' variable is set to whatever (in-temp-file (apheleia-ft--write-temp-file
;; temporary file the formatter will run on (in case it in-text extension))
;; uses the `file' or `filepath' symbol or is a function). (out-temp-file nil)
(in-temp-real-file nil) (command (alist-get (intern formatter) apheleia-formatters))
(out-temp-file nil) (syms nil)
(command (alist-get (intern formatter) apheleia-formatters)) (stdout-buffer nil)
(syms nil) (stderr-file (make-temp-file "apheleia-ft-stderr-"))
(stdout-buffer nil) (default-directory temporary-file-directory)
(stderr-file (make-temp-file "apheleia-ft-stderr-")) (exit-status nil)
(default-directory temporary-file-directory) (out-file (replace-regexp-in-string
(exit-status nil) "/in\\([^/]+\\)" "/out\\1" in-file 'fixedcase))
(out-file (replace-regexp-in-string (exec-path
"/in\\([^/]+\\)" "/out\\1" in-file 'fixedcase)) (append `(,(expand-file-name
(exec-path "scripts/formatters"
(append `(,(expand-file-name (file-name-directory
"scripts/formatters" (file-truename
(file-name-directory ;; Borrowed with love from Magit
(file-truename (let ((load-suffixes '(".el")))
;; Borrowed with love from Magit (locate-library "apheleia"))))))
(let ((load-suffixes '(".el"))) exec-path)))
(locate-library "apheleia")))))) (with-current-buffer (find-file-noselect in-temp-file)
exec-path))) ;; Some formatters use the current file-name or buffer-name to interpret the
;; Some formatters use the current file-name or buffer-name to interpret the ;; type of file that is being formatted. Some may not be able to determine
;; type of file that is being formatted. Some may not be able to determine ;; this from the contents of the file so we set this to force it.
;; this from the contents of the file so we set this to force it. (rename-buffer (file-name-nondirectory in-file))
(rename-buffer (file-name-nondirectory in-file)) (setq stdout-buffer (get-buffer-create
(setq stdout-buffer (get-buffer-create (format "*apheleia-ft-stdout-%S%s" formatter extension)))
(format "*apheleia-ft-stdout-%S%s" formatter extension))) (with-current-buffer stdout-buffer
(with-current-buffer stdout-buffer (erase-buffer))
(erase-buffer)) (if (functionp command)
(if (functionp command) (progn
(let ((in-temp-file (apheleia-ft--write-temp-file
in-text extension)))
(setq in-temp-real-file in-temp-file)
(with-current-buffer (find-file-noselect in-temp-file)
(funcall command (funcall command
:buffer (current-buffer) :buffer (current-buffer)
:scratch (current-buffer) :scratch (current-buffer)
:formatter formatter :formatter formatter
:callback (lambda ())) :callback (lambda ()))
(copy-to-buffer stdout-buffer (point-min) (point-max)))) (copy-to-buffer stdout-buffer (point-min) (point-max)))
(let ((in-temp-file (apheleia-ft--write-temp-file (let ((ctx (apheleia--formatter-context
in-text extension))) (intern formatter) command nil nil)))
(with-current-buffer (find-file-noselect in-temp-file) (setq command `(,(apheleia-formatter--arg1 ctx)
(let ((ctx (apheleia--formatter-context ,@(apheleia-formatter--argv ctx))
(intern formatter) command nil nil))) out-temp-file (apheleia-formatter--output-fname ctx)))
(setq command `(,(apheleia-formatter--arg1 ctx)
,@(apheleia-formatter--argv ctx))
;; In this case the real temp file might be
;; different from the one we generated, because
;; the context creator might generate another
;; temporary file to avoid touching our existing
;; one.
in-temp-real-file (apheleia-formatter--input-fname ctx)
out-temp-file (apheleia-formatter--output-fname ctx))))
(with-current-buffer stdout-buffer (with-current-buffer stdout-buffer
(erase-buffer)) (erase-buffer))
@ -347,16 +340,15 @@ environment variable, defaulting to all formatters."
(error (error
"Formatter %s exited with status %S" formatter exit-status)))) "Formatter %s exited with status %S" formatter exit-status))))
;; Verify that formatter has not touched original file. ;; Verify that formatter has not touched original file.
(when in-temp-real-file (let ((in-text-now (apheleia-ft--read-file in-temp-file)))
(let ((in-text-now (apheleia-ft--read-file in-temp-real-file))) (unless (string= in-text in-text-now)
(unless (string= in-text in-text-now) (apheleia-ft--print-diff
(apheleia-ft--print-diff "original" in-text
"original" in-text "updated" in-text-now)
"updated" in-text-now) (error "Formatter %s modified original file in place" formatter)))
(error "Formatter %s modified original file in place" formatter))))
;; Verify that formatter formatted correctly. ;; Verify that formatter formatted correctly.
(let ((out-text (let ((out-text
(if (or (memq 'output syms) (memq 'inplace syms)) (if out-temp-file
(apheleia-ft--read-file out-temp-file) (apheleia-ft--read-file out-temp-file)
(with-current-buffer stdout-buffer (with-current-buffer stdout-buffer
(buffer-string)))) (buffer-string))))

View file

@ -0,0 +1,2 @@
apt-get install -y python3-pip
pip3 install docformatter

View file

@ -0,0 +1,26 @@
def single_line_doc():
"""
Line break not necessary
"""
def extend_first_line():
"""First line
first line continuation
"""
def add_line_break():
"""First line.
Second line.
"""
def long_lines():
"""
Nullam eu ante vel est convallis dignissim. Fusce suscipit, wisi nec facilisis facilisis, est dui fermentum leo, quis tempor ligula erat quis odio. Nunc porta vulputate tellus. Nunc rutrum turpis sed pede. Sed bibendum. Aliquam posuere. Nunc aliquet, augue nec adipiscing interdum, lacus tellus malesuada massa, quis varius mi purus non odio. Pellentesque condimentum, magna ut suscipit hendrerit, ipsum augue ornare nulla, non luctus diam neque sit amet urna. Curabitur vulputate vestibulum lorem. Fusce sagittis, libero non molestie mollis, magna orci ultrices dolor, at vulputate neque nulla lacinia eros. Sed id ligula quis est convallis tempor. Curabitur lacinia pulvinar nibh. Nam a sapien.
"""

View file

@ -0,0 +1,30 @@
def single_line_doc():
"""Line break not necessary."""
def extend_first_line():
"""First line first line continuation."""
def add_line_break():
"""First line.
Second line.
"""
def long_lines():
"""Nullam eu ante vel est convallis dignissim.
Fusce suscipit, wisi nec facilisis facilisis, est dui fermentum leo,
quis tempor ligula erat quis odio. Nunc porta vulputate tellus.
Nunc rutrum turpis sed pede. Sed bibendum. Aliquam posuere. Nunc
aliquet, augue nec adipiscing interdum, lacus tellus malesuada
massa, quis varius mi purus non odio. Pellentesque condimentum,
magna ut suscipit hendrerit, ipsum augue ornare nulla, non luctus
diam neque sit amet urna. Curabitur vulputate vestibulum lorem.
Fusce sagittis, libero non molestie mollis, magna orci ultrices
dolor, at vulputate neque nulla lacinia eros. Sed id ligula quis
est convallis tempor. Curabitur lacinia pulvinar nibh. Nam a
sapien.
"""