├── LICENSE.md ├── README.md └── python-black.el /LICENSE.md: -------------------------------------------------------------------------------- 1 | BSD-3-clause License 2 | ==================== 3 | 4 | Copyright © 2019, wouter bolsterlee 5 | 6 | All rights reserved. 7 | 8 | Redistribution and use in source and binary forms, with or without 9 | modification, are permitted provided that the following conditions are met: 10 | 11 | * Redistributions of source code must retain the above copyright notice, this 12 | list of conditions and the following disclaimer. 13 | 14 | * Redistributions in binary form must reproduce the above copyright notice, this 15 | list of conditions and the following disclaimer in the documentation and/or 16 | other materials provided with the distribution. 17 | 18 | * Neither the name of the author nor the names of its contributors may be used 19 | to endorse or promote products derived from this software without specific 20 | prior written permission. 21 | 22 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 23 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 24 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 25 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 26 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 27 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 28 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 29 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 30 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 31 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | python-black.el 2 | =============== 3 | 4 | This is an Emacs package to make it easy to [reformat](https://github.com/purcell/reformatter.el) Python code using [black](https://github.com/python/black), the uncompromising Python code formatter. 5 | 6 | As an optional extra, this package can also reformat partial buffers using [black-macchiato](https://github.com/wbolster/black-macchiato), which is a small wrapper around `black` which does just that. 7 | 8 | Installation 9 | ------------ 10 | 11 | Install the [python-black Melpa package](https://melpa.org/#/python-black) using `M-x package-install`, or via [use-package](https://github.com/jwiegley/use-package): 12 | 13 | ``` elisp 14 | (use-package python-black 15 | :demand t 16 | :after python 17 | :hook (python-mode . python-black-on-save-mode-enable-dwim)) 18 | ``` 19 | 20 | Usage 21 | ----- 22 | 23 | Use one of these commands via `M-x` or bind them to a key: 24 | 25 | - `python-black-on-save-mode` 26 | 27 | Minor mode to automatically reformat the buffer on save. 28 | 29 | - `python-black-on-save-mode-enable-dwim` 30 | 31 | Enable `python-black-on-save-mode` if this project is using Black. (Useful in hooks; see example above.) 32 | 33 | - `python-black-buffer` 34 | 35 | Reformat the current buffer. 36 | 37 | - `python-black-region` 38 | 39 | Reformat the current region. (Requires `black-macchiato`.) 40 | 41 | - `python-black-statement` 42 | 43 | Reformat the current statement. (Requires `black-macchiato`.) 44 | 45 | - `python-black-partial-dwim` 46 | 47 | Reformat the active region or the current statement, depending on whether the region is currently active. (Requires `black-macchiato`.) 48 | 49 | - `python-black-org-mode-block` 50 | 51 | Reformat the current `org-mode` Python example or source code block. (Requires `black-macchiato`.) 52 | 53 | Configuration 54 | ------------- 55 | 56 | This package deliberately has minimal configuration. Use `M-x customize-group RET python-black` or change these variables in your `init.el`: 57 | 58 | - `python-black-command` 59 | - `python-black-macchiato-command` 60 | - `python-black-extra-args` 61 | 62 | To configure `black` itself, use an [external configuration file](https://black.readthedocs.io/en/stable/usage_and_configuration/the_basics.html#configuration-via-a-file) for your project, which has the benefits that it can be per-project, and works outside Emacs as well. 63 | 64 | History 65 | ------- 66 | 67 | - next release (already available via melpa unstable) 68 | 69 | - … 70 | 71 | - 1.2.0 (2022-11-03) 72 | 73 | - new `python-black-org-mode-block` function to format an `org-mode` block (#12) 74 | 75 | - `python-black-on-save-mode-enable-dwim` now ignores files in 76 | `site-packages/` directories 77 | 78 | - 1.1.0 (2021-05-11) 79 | 80 | - Add `python-black-on-save-mode-enable-dwim` for use in hooks 81 | - Don't break when there's no newline at the end of the buffer (#4) 82 | 83 | - 1.0.0 (2019-08-17) 84 | 85 | - Initial release 86 | 87 | License 88 | ------- 89 | 90 | BSD-3-clause. Copyright © 2019 wouter bolsterlee. 91 | 92 | Credits 93 | ------- 94 | 95 | wouter bolsterlee. wbolster. 96 | 97 | https://github.com/wbolster on github. star my repos. fork them. and so on. 98 | 99 | https://twitter.com/wbolster on twitter. follow me. or say hi. 100 | -------------------------------------------------------------------------------- /python-black.el: -------------------------------------------------------------------------------- 1 | ;;; python-black.el --- Reformat Python using python-black -*- lexical-binding: t; -*- 2 | 3 | ;; Author: wouter bolsterlee 4 | ;; Keywords: languages 5 | ;; URL: https://github.com/wbolster/emacs-python-black 6 | ;; Package-Requires: ((emacs "25") (dash "2.16.0") (reformatter "0.3")) 7 | ;; Version: 1.2.0 8 | 9 | ;; Copyright 2019 wouter bolsterlee. Licensed under the 3-Clause BSD License. 10 | 11 | ;;; Commentary: 12 | 13 | ;; Commands for reformatting Python code via black (and black-macchiato). 14 | 15 | ;;; Code: 16 | 17 | (require 'dash) 18 | (require 'python) 19 | (require 'reformatter) 20 | (require 'rx) 21 | 22 | (defgroup python-black nil 23 | "Python reformatting using black." 24 | :group 'python 25 | :prefix "python-black-") 26 | 27 | (defcustom python-black-command "black" 28 | "Name of the ‘black’ executable." 29 | :group 'python-black 30 | :type 'string) 31 | 32 | (defcustom python-black-macchiato-command "black-macchiato" 33 | "Name of the ‘black-macchiato’ executable." 34 | :group 'python-black 35 | :type 'string) 36 | 37 | (defvar python-black--base-args '("--quiet") 38 | "Base arguments to pass to black.") 39 | 40 | (defcustom python-black-extra-args nil 41 | "Extra arguments to pass to black." 42 | :group 'python-black 43 | :type '(repeat string)) 44 | 45 | (defconst python-black--config-file "pyproject.toml") 46 | (defconst python-black--config-file-marker-regex (rx bol "[tool.black]" eol)) 47 | 48 | ;;;###autoload (autoload 'python-black-buffer "python-black" nil t) 49 | ;;;###autoload (autoload 'python-black-region "python-black" nil t) 50 | ;;;###autoload (autoload 'python-black-on-save-mode "python-black" nil t) 51 | (reformatter-define python-black 52 | :program (python-black--command beg end) 53 | :args (python-black--make-args beg end) 54 | :lighter " BlackFMT" 55 | :group 'python-black) 56 | 57 | ;;;###autoload 58 | (defun python-black-on-save-mode-enable-dwim () 59 | "Enable ‘python-black-on-save-mode’ if appropriate." 60 | (interactive) 61 | (-when-let* ((file-name (buffer-file-name)) 62 | (uses-black? (python-black--in-blackened-project-p file-name)) 63 | (not-third-party? (not (python-black--third-party-file-p file-name)))) 64 | (python-black-on-save-mode))) 65 | 66 | ;;;###autoload 67 | (defun python-black-statement (&optional display-errors) 68 | "Reformats the current statement. 69 | 70 | When called interactively with a prefix argument, or when 71 | DISPLAY-ERRORS is non-nil, shows a buffer if the formatting fails." 72 | (interactive "p") 73 | (-when-let* ((beg (save-excursion 74 | (python-nav-beginning-of-statement) 75 | (line-beginning-position))) 76 | (end (save-excursion 77 | (python-nav-end-of-statement) 78 | (line-end-position))) 79 | (non-empty? (not (= beg end)))) 80 | (python-black-region beg (min (point-max) (1+ end)) display-errors))) 81 | 82 | ;;;###autoload 83 | (defun python-black-partial-dwim (&optional display-errors) 84 | "Reformats the active region or the current statement. 85 | 86 | This runs ‘python-black-region’ or ‘python-black-statement’ depending 87 | on whether the region is currently active. 88 | 89 | When called interactively with a prefix argument, or when 90 | DISPLAY-ERRORS is non-nil, shows a buffer if the formatting fails." 91 | (interactive "p") 92 | (if (region-active-p) 93 | (python-black-region (region-beginning) (region-end) display-errors) 94 | (python-black-statement display-errors))) 95 | 96 | (declare-function org-in-block-p "org" (names)) 97 | (declare-function org-src--contents-area "org-src" (datum)) 98 | (declare-function org-element-at-point "org-element" (&optional pom cached-only)) 99 | (declare-function org-unescape-code-in-region "org-src" (beg end)) 100 | (declare-function org-escape-code-in-region "org-src" (beg end)) 101 | 102 | ;;;###autoload 103 | (defun python-black-org-mode-block (&optional display-errors) 104 | "Reformats the current `org-mode' source block. 105 | When called interactively, or with prefix argument 106 | DISPLAY-ERRORS, shows a buffer if the formatting fails." 107 | (interactive) 108 | (unless (org-in-block-p '("src" "example")) 109 | (user-error "Not in a source block")) 110 | (save-mark-and-excursion 111 | (pcase (org-src--contents-area (org-element-at-point)) 112 | (`(,beg ,end ,_) 113 | (let ((buf (current-buffer)) 114 | (end (- end 1))) 115 | (with-temp-buffer 116 | (let ((temp-buf (current-buffer))) 117 | (insert-buffer-substring-no-properties buf beg end) 118 | (org-unescape-code-in-region (point-min) (point-max)) 119 | (python-black-region (point-min) (point-max) display-errors) 120 | (org-escape-code-in-region (point-min) (point-max)) 121 | (with-current-buffer buf 122 | (save-restriction 123 | (narrow-to-region beg end) 124 | (replace-buffer-contents temp-buf 0.1)))))))))) 125 | 126 | (defun python-black--command (beg end) 127 | "Helper to decide which command to run for span BEG to END." 128 | (if (python-black--whole-buffer-p beg end) 129 | python-black-command 130 | (unless (executable-find python-black-macchiato-command) 131 | (error "Partial formatting requires ‘%s’, but it is not installed" 132 | python-black-macchiato-command)) 133 | python-black-macchiato-command)) 134 | 135 | (defun python-black--make-args (beg end) 136 | "Helper to build the argument list for black for span BEG to END." 137 | (append 138 | python-black--base-args 139 | (-when-let* ((file-name (buffer-file-name)) 140 | (extension (file-name-extension file-name)) 141 | (is-pyi-file (string-equal "pyi" extension))) 142 | '("--pyi")) 143 | python-black-extra-args 144 | (when (python-black--whole-buffer-p beg end) 145 | '("-")))) 146 | 147 | (defun python-black--whole-buffer-p (beg end) 148 | "Return whether BEG and END span the whole buffer." 149 | (and (= (point-min) beg) 150 | (= (point-max) end))) 151 | 152 | (defun python-black--in-blackened-project-p (file-name) 153 | "Determine whether FILE-NAME resides in a project that is using Black. 154 | 155 | This looks for ‘[tool.black]’ in a ‘pyproject.toml’ file." 156 | (-when-let* ((project-directory (locate-dominating-file file-name python-black--config-file)) 157 | (config-file (concat project-directory python-black--config-file)) 158 | (config-file-contains-marker 159 | (with-temp-buffer 160 | (insert-file-contents-literally config-file) 161 | (re-search-forward python-black--config-file-marker-regex nil t 1)))) 162 | t)) 163 | 164 | (defun python-black--third-party-file-p (file-name) 165 | "Determine whether FILE-NAME is likely a third party file." 166 | (-when-let* ((lib-python-dir (locate-dominating-file file-name "site-packages"))) 167 | t)) 168 | 169 | (provide 'python-black) 170 | ;;; python-black.el ends here 171 | --------------------------------------------------------------------------------