summaryrefslogtreecommitdiff
path: root/emacs.d/elpa/blacken-20191123.1547/blacken.el
blob: ded35e0bd86b8f54a12337c33a09093f3e9179d2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
;;; blacken.el --- Reformat python buffers using the "black" formatter

;; Copyright (C) 2018-2019 Artem Malyshev

;; Author: Artem Malyshev <proofit404@gmail.com>
;; Homepage: https://github.com/proofit404/blacken
;; Version: 0.0.1
;; Package-Version: 20191123.1547
;; Package-Requires: ((emacs "25.2"))

;; This file is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published
;; by the Free Software Foundation; either version 3, or (at your
;; option) any later version.
;;
;; This file is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.
;;
;; For a full copy of the GNU General Public License
;; see <http://www.gnu.org/licenses/>.

;;; Commentary:
;;
;; Blacken uses black to format a Python buffer.  It can be called
;; explicitly on a certain buffer, but more conveniently, a minor-mode
;; 'blacken-mode' is provided that turns on automatically running
;; black on a buffer before saving.
;;
;; Installation:
;;
;; Add blacken.el to your load-path.
;;
;; To automatically format all Python buffers before saving, add the
;; function blacken-mode to python-mode-hook:
;;
;; (add-hook 'python-mode-hook 'blacken-mode)
;;
;;; Code:

(require 'cl-lib)

(defgroup blacken nil
  "Reformat Python code with \"black\"."
  :group 'python)

(defcustom blacken-executable "black"
  "Name of the executable to run."
  :type 'string)

(defcustom blacken-line-length nil
  "Line length to enforce.

It must be an integer, nil or `fill'.
If `fill', the `fill-column' variable value is used."
  :type '(choice :tag "Line Length Limit"
           (const :tag "Use default" nil)
           (const :tag "Use fill-column" fill)
           (integer :tag "Line Length"))
  :safe 'integerp)

(defcustom blacken-allow-py36 nil
  "Allow using Python 3.6-only syntax on all input files."
  :type 'boolean
  :safe 'booleanp)

(defcustom blacken-skip-string-normalization nil
  "Don't normalize string quotes or prefixes."
  :type 'boolean
  :safe 'booleanp)

(defcustom blacken-fast-unsafe nil
  "Skips temporary sanity checks."
  :type 'boolean
  :safe 'booleanp)

(defcustom blacken-only-if-project-is-blackened nil
  "Only blacken if project has a pyproject.toml with a [tool.black] section."
  :type 'boolean
  :safe 'booleanp)

(defun blacken-call-bin (input-buffer output-buffer error-buffer)
  "Call process black.

Send INPUT-BUFFER content to the process stdin.  Saving the
output to OUTPUT-BUFFER.  Saving process stderr to ERROR-BUFFER.
Return black process the exit code."
  (with-current-buffer input-buffer
    (let ((process (make-process :name "blacken"
                                 :command `(,blacken-executable ,@(blacken-call-args))
                                 :buffer output-buffer
                                 :stderr error-buffer
                                 :noquery t
                                 :sentinel (lambda (process event)))))
      (set-process-query-on-exit-flag (get-buffer-process error-buffer) nil)
      (set-process-sentinel (get-buffer-process error-buffer) (lambda (process event)))
      (save-restriction
        (widen)
        (process-send-region process (point-min) (point-max)))
      (process-send-eof process)
      (accept-process-output process nil nil t)
      (while (process-live-p process)
        (accept-process-output process nil nil t))
      (process-exit-status process))))

(defun blacken-call-args ()
  "Build black process call arguments."
  (append
   (when blacken-line-length
     (list "--line-length"
           (number-to-string (cl-case blacken-line-length
                               ('fill fill-column)
                               (t blacken-line-length)))))
   (when blacken-allow-py36
     (list "--py36"))
   (when blacken-fast-unsafe
     (list "--fast"))
   (when blacken-skip-string-normalization
     (list "--skip-string-normalization"))
   (when (and (buffer-file-name (current-buffer))
              (string-match "\.pyi$" (buffer-file-name (current-buffer))))
     (list "--pyi"))
   '("-")))

(defun blacken-project-is-blackened (&optional display)
  "Whether the project has a pyproject.toml with [tool.black] in it."
  (when-let ((parent (locate-dominating-file default-directory "pyproject.toml")))
    (with-temp-buffer
      (insert-file-contents (concat parent "pyproject.toml"))
      (re-search-forward "^\\[tool.black\\]$" nil t 1))))

;;;###autoload
(defun blacken-buffer (&optional display)
  "Try to blacken the current buffer.

Show black output, if black exit abnormally and DISPLAY is t."
  (interactive (list t))
  (let* ((original-buffer (current-buffer))
         (original-point (point))
         (original-window-pos (window-start))
         (tmpbuf (get-buffer-create "*blacken*"))
         (errbuf (get-buffer-create "*blacken-error*")))
    ;; This buffer can be left after previous black invocation.  It
    ;; can contain error message of the previous run.
    (dolist (buf (list tmpbuf errbuf))
      (with-current-buffer buf
        (erase-buffer)))
    (condition-case err
        (if (not (zerop (blacken-call-bin original-buffer tmpbuf errbuf)))
            (error "Black failed, see %s buffer for details" (buffer-name errbuf))
          (unless (eq (compare-buffer-substrings tmpbuf nil nil original-buffer nil nil) 0)
            (with-current-buffer tmpbuf
              (copy-to-buffer original-buffer (point-min) (point-max)))
            (goto-char original-point)
            (set-window-start (selected-window) original-window-pos))
          (mapc 'kill-buffer (list tmpbuf errbuf)))
      (error (message "%s" (error-message-string err))
             (when display
               (with-current-buffer errbuf
                 (setq-local scroll-conservatively 0))
               (pop-to-buffer errbuf))))))

;;;###autoload
(define-minor-mode blacken-mode
  "Automatically run black before saving."
  :lighter " Black"
  (if blacken-mode
      (when (or (not blacken-only-if-project-is-blackened)
                (blacken-project-is-blackened))
        (add-hook 'before-save-hook 'blacken-buffer nil t))
    (remove-hook 'before-save-hook 'blacken-buffer t)))

(provide 'blacken)

;;; blacken.el ends here