├── tests ├── test_cases │ ├── stub.pyi │ ├── hello_world.py │ ├── mixed_tab_spaces.py │ └── no_closing_bracket.py ├── conflicting_configurations │ ├── pyproject.toml │ ├── goodbye.py │ ├── hello.py │ └── .flake8 ├── non_conflicting_configurations │ ├── pyproject.toml │ ├── hello.py │ └── .flake8 ├── test_changes │ ├── commas.txt │ ├── hello_world.txt │ ├── black_preview.txt │ ├── hello_world_EOF.txt │ ├── hello_world_EOF.py │ ├── hello_world.py │ ├── black_preview.py │ └── commas.py ├── with_bad_toml │ ├── hello_world.txt │ ├── pyproject.toml │ └── hello_world.py ├── with_pyproject_toml │ ├── ignoring_toml.txt │ ├── pyproject.toml │ └── ordinary_quotes.py ├── without_pyproject_toml │ └── ordinary_quotes.py └── run_tests.sh ├── MANIFEST.in ├── requirements.txt ├── .gitignore ├── LICENSE.rst ├── .flake8 ├── .github └── workflows │ └── test.yml ├── pyproject.toml ├── .pre-commit-config.yaml ├── flake8_black.py └── README.rst /tests/test_cases/stub.pyi: -------------------------------------------------------------------------------- 1 | def my_function(): ... 2 | 3 | class Wibble: ... 4 | -------------------------------------------------------------------------------- /tests/conflicting_configurations/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | line-length = 90 3 | -------------------------------------------------------------------------------- /tests/non_conflicting_configurations/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | line-length = 120 3 | -------------------------------------------------------------------------------- /tests/test_changes/commas.txt: -------------------------------------------------------------------------------- 1 | test_changes/commas.py:18:10: BLK100 Black would make changes. 2 | -------------------------------------------------------------------------------- /tests/with_bad_toml/hello_world.txt: -------------------------------------------------------------------------------- 1 | with_bad_toml/hello_world.py:0:1: BLK997 Invalid TOML file 2 | -------------------------------------------------------------------------------- /tests/test_changes/hello_world.txt: -------------------------------------------------------------------------------- 1 | test_changes/hello_world.py:12:6: BLK100 Black would make changes. 2 | -------------------------------------------------------------------------------- /tests/test_changes/black_preview.txt: -------------------------------------------------------------------------------- 1 | test_changes/black_preview.py:13:25: BLK100 Black would make changes. 2 | -------------------------------------------------------------------------------- /tests/test_changes/hello_world_EOF.txt: -------------------------------------------------------------------------------- 1 | test_changes/hello_world_EOF.py:11:1: BLK100 Black would make changes. 2 | -------------------------------------------------------------------------------- /tests/with_pyproject_toml/ignoring_toml.txt: -------------------------------------------------------------------------------- 1 | with_pyproject_toml/ordinary_quotes.py:10:7: BLK100 Black would make changes. 2 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include LICENSE.rst 3 | include requirements.txt 4 | recursive-include tests *.py *.txt *.sh *.toml 5 | -------------------------------------------------------------------------------- /tests/conflicting_configurations/goodbye.py: -------------------------------------------------------------------------------- 1 | """Print 'Au revoir' to the terminal. 2 | 3 | This is a simple test script. 4 | """ 5 | 6 | print("Au revoir") 7 | -------------------------------------------------------------------------------- /tests/with_bad_toml/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | skip-string-normalization = true 3 | # This line is (a) in the wrong file, and (b) invalid syntax 4 | black-config= 5 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # This is a plugin for flake8, so we require that. 2 | # Our prefix is not single letter, so we need v3: 3 | flake8 >= 3.0.0 4 | 5 | # We need black 6 | black >= 22.1.0 7 | tomli ; python_version < "3.11" 8 | -------------------------------------------------------------------------------- /tests/with_pyproject_toml/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | # This is probably better as an integer, 3 | # but reasonable to cope with it as str: 4 | line-length = "88" 5 | skip-string-normalization = true 6 | skip-magic-trailing-comma = true 7 | preview = true 8 | -------------------------------------------------------------------------------- /tests/test_cases/hello_world.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """Print 'Hello world' to the terminal. 4 | 5 | This is a simple test script using a hashbang line 6 | and a PEP263 encoding line. 7 | """ 8 | 9 | print("Hello world") 10 | -------------------------------------------------------------------------------- /tests/with_bad_toml/hello_world.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """Print 'Hello world' to the terminal. 4 | 5 | This is a simple test script using a hashbang line 6 | and a PEP263 encoding line. 7 | """ 8 | 9 | print("Hello world") 10 | -------------------------------------------------------------------------------- /tests/conflicting_configurations/hello.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """Print 'Hello world' to the terminal. 4 | 5 | This is a simple test script using a hashbang line 6 | and a PEP263 encoding line. 7 | """ 8 | 9 | print("Hello world") 10 | -------------------------------------------------------------------------------- /tests/non_conflicting_configurations/hello.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """Print 'Hello world' to the terminal. 4 | 5 | This is a simple test script using a hashbang line 6 | and a PEP263 encoding line. 7 | """ 8 | 9 | print("Hello world") 10 | -------------------------------------------------------------------------------- /tests/conflicting_configurations/.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | 3 | # Plugin flake8-black will pass this value to black 4 | max-line-length = 120 5 | 6 | extend-ignore = 7 | # See https://github.com/PyCQA/pycodestyle/issues/373 8 | # flake8/pycodechecker give false positives on black code 9 | E203, 10 | -------------------------------------------------------------------------------- /tests/non_conflicting_configurations/.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | 3 | # Plugin flake8-black will pass this value to black 4 | max-line-length = 120 5 | 6 | extend-ignore = 7 | # See https://github.com/PyCQA/pycodestyle/issues/373 8 | # flake8/pycodechecker give false positives on black code 9 | E203, 10 | -------------------------------------------------------------------------------- /tests/test_cases/mixed_tab_spaces.py: -------------------------------------------------------------------------------- 1 | """Invalid under Python 3, example with mixed indentation.""" 2 | 3 | if True: 4 | print("This line was indented with four spaces!") 5 | if True: 6 | print("This line was indented with eight spaces.") 7 | if True: 8 | print("This line was indented with a tab!") 9 | -------------------------------------------------------------------------------- /tests/with_pyproject_toml/ordinary_quotes.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """Print 'Hello world' to the terminal. 4 | 5 | This is a simple test script using a hashbang line 6 | and a PEP263 encoding line. 7 | We use ordinary quotes in this test. 8 | """ 9 | 10 | print('Hello world') 11 | -------------------------------------------------------------------------------- /tests/without_pyproject_toml/ordinary_quotes.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """Print 'Hello world' to the terminal. 4 | 5 | This is a simple test script using a hashbang line 6 | and a PEP263 encoding line. 7 | We use ordinary quotes in this test. 8 | """ 9 | 10 | print('Hello world') 11 | -------------------------------------------------------------------------------- /tests/test_changes/hello_world_EOF.py: -------------------------------------------------------------------------------- 1 | """Print 'Hello world' to the terminal. 2 | 3 | This is a simple test script which in the formal form has a missing final 4 | line break - which black will add. 5 | 6 | The point of this is the edit position will be at the very end of the file, 7 | which is a corner case. 8 | """ 9 | 10 | print("Hello world") -------------------------------------------------------------------------------- /tests/test_changes/hello_world.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """Print 'Hello world' to the terminal. 4 | 5 | This is a simple test script using a hashbang line and a PEP263 encoding line. 6 | 7 | In the original format, there excess spaces in the print call, and unwanted 8 | blank lines - which black will remove. 9 | """ 10 | 11 | 12 | print ( "Hello world" ) 13 | 14 | -------------------------------------------------------------------------------- /tests/test_changes/black_preview.py: -------------------------------------------------------------------------------- 1 | """Example showing future black string reformatting.""" 2 | 3 | a_very_long_variable = 17.3 4 | short_value = 1234567890 5 | 6 | 7 | def and_a_very_long_function_call(): 8 | """Deep thought.""" 9 | return 42 10 | 11 | 12 | my_dict = { 13 | "a key in my dict": a_very_long_variable 14 | * and_a_very_long_function_call() 15 | / 100000.0, 16 | "another key": (short_value), 17 | } 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | #Ignore the build directory (and its sub-directories): 2 | build 3 | 4 | #Ignore the distribution directory 5 | dist 6 | 7 | #Ignore another Python specific build folder: 8 | flake8_black.egg-info/ 9 | 10 | #Ignore backup files from some Unix editors, 11 | *~ 12 | *.swp 13 | *.bak 14 | 15 | #Ignore patches and any original files created by patch command 16 | *.diff 17 | *.orig 18 | *.rej 19 | 20 | #Ignore these hidden files from Mac OS X 21 | .DS_Store 22 | 23 | #Ignore hidden files from Dolphin window manager 24 | .directory 25 | -------------------------------------------------------------------------------- /tests/test_changes/commas.py: -------------------------------------------------------------------------------- 1 | """Example of black and magic commas.""" 2 | 3 | vegetables = { 4 | "carrot", 5 | "parsnip", 6 | "potato", 7 | "swede", 8 | "leak", 9 | "aubergine", 10 | "tomato", 11 | "peas", 12 | "beans", 13 | } 14 | 15 | # This set would easily fit on one line, but a trailing comma 16 | # after the final entry tells black (by default) to leave this 17 | # with one entry per line: 18 | yucky = { 19 | "aubergine", 20 | "squid", 21 | "snails", 22 | } 23 | 24 | print("I dislike these vegetables: %s." % ", ".join(vegetables.intersection(yucky))) 25 | -------------------------------------------------------------------------------- /tests/test_cases/no_closing_bracket.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """Print 'Hello world' to the terminal (with syntax error). 4 | 5 | This is a simple test script using a hashbang line and a PEP263 encoding line. There is 6 | a deliberate syntax error (missing closing bracket). 7 | 8 | Black will fail to parse this file: 9 | 10 | $ black --check no_closing_bracket.py ; echo "Return code $?" 11 | error: cannot format no_closing_bracket.py: ('EOF in multi-line statement', (31, 0)) 12 | All done! ð ð ð 13 | 1 file would fail to reformat. 14 | Return code 123 15 | 16 | It seems in this case the plugin never gets the chance to report: 17 | 18 | $ flake8 --select BLK no_closing_bracket.py ; echo "Return code $?" 19 | Return code 0 20 | 21 | This doesn't really matter, as it would be redundant with the flake8 syntax errors: 22 | 23 | $ flake8 no_closing_bracket.py ; echo "Return code $?" 24 | no_closing_bracket.py:30:19: E999 SyntaxError: unexpected EOF while parsing 25 | Return code 1 26 | 27 | """ 28 | 29 | print("Hello world" 30 | -------------------------------------------------------------------------------- /LICENSE.rst: -------------------------------------------------------------------------------- 1 | Copyright 2019, Peter Cock, The James Hutton Institute, UK. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | 3 | # Recommend matching the black line length (default 88), 4 | # rather than using the flake8 default of 79: 5 | max-line-length = 88 6 | 7 | extend-ignore = 8 | # See https://github.com/PyCQA/pycodestyle/issues/373 9 | # flake8/pycodechecker give false positives on black code 10 | E203, 11 | 12 | # Most of our test cases deliberately violate style checks: 13 | per-file-ignores = 14 | # These are meant to trigger black changes: 15 | tests/test_changes/hello_world.py: E201,E202,E211,W391,BLK100 16 | tests/test_changes/hello_world_EOF.py: W292,BLK100 17 | tests/without_pyproject_toml/ordinary_quotes.py: Q000,BLK100 18 | # These are not meant to trigger black changes: 19 | tests/test_cases/no_closing_bracket.py: E902 20 | tests/test_fail/mixed_tab_spaces.py: E101,E999,W191 21 | tests/with_pyproject_toml/ordinary_quotes.py: Q000 22 | tests/test_cases/mixed_tab_spaces.py: E101,E999,W191 23 | # The bad TOML file breaks black checking this file: 24 | tests/with_bad_toml/hello_world.py: BLK997, 25 | 26 | # ===================== 27 | # flake-quote settings: 28 | # ===================== 29 | # Set this to match black style: 30 | inline-quotes = double 31 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | run-tests: 7 | runs-on: ${{ matrix.os }} 8 | defaults: 9 | run: 10 | shell: bash 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | os: [windows-latest, macos-latest, ubuntu-latest] 15 | python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] 16 | black-version: ["23.9.1", "25.9.0"] 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Set up Python ${{ matrix.python-version }} 20 | uses: actions/setup-python@v2 21 | with: 22 | python-version: ${{ matrix.python-version }} 23 | - name: Git configuration 24 | run: | 25 | git config core.autocrlf false 26 | git reset --hard 27 | - name: Build packages, and install the wheel 28 | run: | 29 | pip install build black==${{ matrix.black-version }} 30 | python -m build 31 | cd dist 32 | python -m pip install flake8_black-*.whl 33 | cd .. 34 | # Wheel should now be installed 35 | flake8 --version 36 | - name: Run tests 37 | run: | 38 | # Unpack the tests from the sdist tar-ball 39 | # (want to confirm the manifest was complete) 40 | tar -zxvf dist/flake8_black-*.tar.gz 41 | cd flake8_black-*/tests/ 42 | WIN="$([ "$RUNNER_OS" == "Windows" ];echo $?)" bash ./run_tests.sh 43 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ['pip>=21.3', 'setuptools>=61', 'wheel'] 3 | build-backend = 'setuptools.build_meta' 4 | 5 | [project] 6 | name = 'flake8-black' 7 | description = 'flake8 plugin to call black as a code style validator' 8 | keywords = ['black', 'formatting', 'style', 'flake8'] 9 | license = {text = 'MIT'} 10 | readme = 'README.rst' 11 | authors = [ 12 | {name = 'Peter J. A. Cock'} 13 | ] 14 | maintainers = [ 15 | {name = 'Peter J. A. Cock', email = 'p.j.a.cock@googlemail.com'} 16 | ] 17 | classifiers = [ 18 | 'Intended Audience :: Developers', 19 | 'Framework :: Flake8', 20 | 'License :: OSI Approved :: MIT License', 21 | 'Operating System :: OS Independent', 22 | 'Topic :: Software Development :: Libraries :: Python Modules', 23 | 'Topic :: Software Development :: Quality Assurance', 24 | 'Programming Language :: Python', 25 | 'Programming Language :: Python :: 3', 26 | 'Programming Language :: Python :: 3 :: Only' 27 | ] 28 | requires-python = '>=3.9' 29 | dependencies = [ 30 | 'flake8>=3', 31 | 'black>=22.1.0', 32 | 'tomli ; python_version < "3.11"', 33 | ] 34 | dynamic = ['version'] 35 | [project.entry-points] 36 | 'flake8.extension' = {BLK = 'flake8_black:BlackStyleChecker'} 37 | [project.optional-dependencies] 38 | develop = ['build', 'twine'] 39 | [project.urls] 40 | Homepage = 'https://github.com/peterjc/flake8-black' 41 | 'Source Code' = 'https://github.com/peterjc/flake8-black/' 42 | 'Bug Tracker' = 'https://github.com/peterjc/flake8-black/issues' 43 | Documentation = 'https://github.com/peterjc/flake8-black/blob/master/README.rst' 44 | 45 | [tool.setuptools] 46 | py-modules = ['flake8_black'] 47 | zip-safe = true 48 | [tool.setuptools.dynamic] 49 | version = {attr = 'flake8_black.__version__'} 50 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # pre-commit run --all-files 2 | repos: 3 | - repo: https://github.com/pre-commit/pre-commit-hooks 4 | rev: v6.0.0 5 | hooks: 6 | - id: check-added-large-files 7 | - id: check-case-conflict 8 | - id: check-executables-have-shebangs 9 | - id: check-merge-conflict 10 | - id: check-shebang-scripts-are-executable 11 | - id: check-symlinks 12 | - id: check-yaml 13 | - id: debug-statements 14 | exclude: tests/ 15 | - id: destroyed-symlinks 16 | - id: end-of-file-fixer 17 | exclude: tests/test_changes/ 18 | files: \.(py|sh|rst|yml|yaml)$ 19 | - id: mixed-line-ending 20 | - id: trailing-whitespace 21 | files: \.(py|sh|rst|yml|yaml)$ 22 | - repo: https://github.com/psf/black-pre-commit-mirror 23 | rev: 25.11.0 24 | hooks: 25 | - id: black 26 | exclude: tests/ 27 | args: [--check] 28 | - repo: https://github.com/PyCQA/flake8 29 | rev: 7.3.0 30 | hooks: 31 | - id: flake8 32 | additional_dependencies: [ 33 | 'flake8-blind-except', 34 | 'flake8-docstrings', 35 | 'flake8-bugbear', 36 | 'flake8-comprehensions', 37 | 'flake8-docstrings', 38 | 'flake8-implicit-str-concat', 39 | 'pydocstyle>=5.0.0', 40 | ] 41 | exclude: ^tests/test_cases/no_closing_bracket\.py$ 42 | - repo: https://github.com/asottile/blacken-docs 43 | rev: 1.20.0 44 | hooks: 45 | - id: blacken-docs 46 | additional_dependencies: [black==24.8.0] 47 | exclude: ^.github/ 48 | - repo: https://github.com/rstcheck/rstcheck 49 | rev: v6.2.5 50 | hooks: 51 | - id: rstcheck 52 | args: [ 53 | --report-level=warning, 54 | ] 55 | - repo: https://github.com/codespell-project/codespell 56 | rev: v2.4.1 57 | hooks: 58 | - id: codespell 59 | files: \.(py|sh|rst|yml|yaml)$ 60 | ci: 61 | # Settings for the https://pre-commit.ci/ continuous integration service 62 | autofix_prs: true 63 | # Default message is more verbose 64 | autoupdate_commit_msg: '[pre-commit.ci] autoupdate' 65 | # Default is weekly 66 | autoupdate_schedule: monthly 67 | -------------------------------------------------------------------------------- /tests/run_tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | 5 | # Assumes in the tests/ directory 6 | 7 | echo "Checking our configuration option appears in help" 8 | flake8 -h 2>&1 | grep "black-config" 9 | 10 | set +o pipefail 11 | 12 | echo "Checking we report an error when can't find specified config file" 13 | flake8 --black-config does_not_exist.toml 2>&1 | grep -i "could not find" 14 | 15 | echo "Checking failure with mal-formed TOML file" 16 | flake8 --select BLK test_cases/ --black-config with_bad_toml/pyproject.toml 2>&1 | grep -i "could not parse" 17 | 18 | set -o pipefail 19 | 20 | echo "Checking we report no errors on these test cases" 21 | # Must explicitly include *.pyi or flake8 ignores them 22 | flake8 --select BLK test_cases/*.py* 23 | # Adding --black-config '' meaning ignore any pyproject.toml should have no effect: 24 | flake8 --select BLK test_cases/*.py --black-config '' 25 | flake8 --select BLK --max-line-length 50 test_cases/*.py 26 | flake8 --select BLK --max-line-length 90 test_cases/*.py 27 | flake8 --select BLK with_pyproject_toml/*.py 28 | flake8 --select BLK with_pyproject_toml/*.py --black-config with_pyproject_toml/pyproject.toml 29 | flake8 --select BLK --max-line-length 88 with_pyproject_toml/ 30 | flake8 --select BLK without_pyproject_toml/*.py --black-config with_pyproject_toml/pyproject.toml 31 | # Adding --black-config '' should have no effect: 32 | #flake8 --select BLK --max-line-length 88 with_pyproject_toml/ --black-config '' 33 | flake8 --select BLK non_conflicting_configurations/*.py 34 | flake8 --select BLK conflicting_configurations/*.py 35 | # Here using --black-config '' meaning ignore any (bad) pyproject.toml files: 36 | flake8 --select BLK with_bad_toml/hello_world.py --black-config '' 37 | 38 | echo "Checking we report expected black changes" 39 | diff test_changes/hello_world.txt <(flake8 --select BLK test_changes/hello_world.py) 40 | diff test_changes/hello_world_EOF.txt <(flake8 --select BLK test_changes/hello_world_EOF.py) 41 | diff test_changes/hello_world_EOF.txt <(flake8 --select BLK test_changes/hello_world_EOF.py --black-config '') 42 | diff <( 43 | if [ "${WIN:-1}" = 0 ]; then 44 | sed 's_/_\\_2' with_bad_toml/hello_world.txt 45 | else 46 | cat with_bad_toml/hello_world.txt 47 | fi 48 | ) <(flake8 --select BLK with_bad_toml/hello_world.py) 49 | diff with_pyproject_toml/ignoring_toml.txt <(flake8 with_pyproject_toml/ --select BLK --black-config '') 50 | 51 | # no changes by default, 52 | flake8 --select BLK test_changes/commas.py tests/black_preview.py 53 | # will make changes if we ignore the magic trailing comma: 54 | diff test_changes/commas.txt <(flake8 --select BLK test_changes/commas.py --black-config with_pyproject_toml/pyproject.toml) 55 | # will make changes if we enable future functionality preview mode: 56 | diff test_changes/black_preview.txt <(flake8 --select BLK test_changes/black_preview.py --black-config with_pyproject_toml/pyproject.toml) 57 | 58 | echo "Tests passed." 59 | -------------------------------------------------------------------------------- /flake8_black.py: -------------------------------------------------------------------------------- 1 | """Check Python code passes black style validation via flake8. 2 | 3 | This is a plugin for the tool flake8 tool for checking Python 4 | source code using the tool black. 5 | """ 6 | 7 | import sys 8 | from os import path 9 | from pathlib import Path 10 | 11 | if sys.version_info >= (3, 11): 12 | import tomllib 13 | else: 14 | import tomli as tomllib 15 | 16 | import black 17 | 18 | from flake8 import utils as stdin_utils 19 | from flake8 import LOG 20 | 21 | 22 | __version__ = "0.4.0" 23 | 24 | black_prefix = "BLK" 25 | 26 | 27 | try: 28 | black.decode_bytes(b"") 29 | except TypeError: 30 | # TypeError: decode_bytes() missing required argument 'mode' (pos 2) 31 | decode_bytes_wrapper = black.decode_bytes 32 | else: 33 | # Probably running older than black 25.9.0 34 | def decode_bytes_wrapper(src, mode): 35 | """Proxy wrapper for backward compatibility.""" 36 | return black.decode_bytes(src) 37 | 38 | 39 | def find_diff_start(old_src, new_src): 40 | """Find line number and column number where text first differs.""" 41 | old_lines = old_src.split("\n") 42 | new_lines = new_src.split("\n") 43 | 44 | for line in range(min(len(old_lines), len(new_lines))): 45 | old = old_lines[line] 46 | new = new_lines[line] 47 | if old == new: 48 | continue 49 | for col in range(min(len(old), len(new))): 50 | if old[col] != new[col]: 51 | return line, col 52 | # Difference at the end of the line... 53 | return line, min(len(old), len(new)) 54 | # Difference at the end of the file... 55 | return min(len(old_lines), len(new_lines)), 0 56 | 57 | 58 | class BadBlackConfig(ValueError): 59 | """Bad black TOML configuration file.""" 60 | 61 | pass 62 | 63 | 64 | def load_black_mode(toml_filename=None): 65 | """Load black configuration TOML file (or return defaults) as black.Mode object.""" 66 | if not toml_filename: 67 | return black.FileMode( 68 | target_versions=set(), 69 | line_length=black.DEFAULT_LINE_LENGTH, # Expect to be 88 70 | string_normalization=True, 71 | magic_trailing_comma=True, 72 | preview=False, 73 | ) 74 | 75 | LOG.info("flake8-black: loading black settings from %s", toml_filename) 76 | try: 77 | with toml_filename.open(mode="rb") as toml_file: 78 | pyproject_toml = tomllib.load(toml_file) 79 | except ValueError: 80 | LOG.info("flake8-black: invalid TOML file %s", toml_filename) 81 | raise BadBlackConfig(path.relpath(toml_filename)) 82 | config = pyproject_toml.get("tool", {}).get("black", {}) 83 | black_config = {k.replace("--", "").replace("-", "_"): v for k, v in config.items()} 84 | 85 | # Extract the fields we care about, 86 | # cast to int explicitly otherwise line length could be a string 87 | return black.Mode( 88 | target_versions={ 89 | black.TargetVersion[val.upper()] 90 | for val in black_config.get("target_version", []) 91 | }, 92 | line_length=int(black_config.get("line_length", black.DEFAULT_LINE_LENGTH)), 93 | string_normalization=not black_config.get("skip_string_normalization", False), 94 | magic_trailing_comma=not black_config.get("skip_magic_trailing_comma", False), 95 | preview=bool(black_config.get("preview", False)), 96 | ) 97 | 98 | 99 | black_config = {None: load_black_mode()} # None key's value is default config 100 | 101 | 102 | class BlackStyleChecker: 103 | """Checker of Python code using black.""" 104 | 105 | name = "black" 106 | version = __version__ 107 | override_config = None 108 | 109 | STDIN_NAMES = {"stdin", "-", "(none)", None} 110 | 111 | def __init__(self, tree, filename="(none)"): 112 | """Initialise.""" 113 | self.tree = tree 114 | self.filename = filename 115 | 116 | @property 117 | def _file_mode(self): 118 | """Return black.FileMode object, using local pyproject.toml as needed.""" 119 | if self.override_config: 120 | return self.override_config 121 | 122 | # Unless using override, we look for pyproject.toml 123 | project_root = black.find_project_root( 124 | ("." if self.filename in self.STDIN_NAMES else self.filename,) 125 | ) 126 | if isinstance(project_root, tuple): 127 | # black stable 22.1.0 update find_project_root return value 128 | # from Path to Tuple[Path, str] 129 | project_root = project_root[0] 130 | path = project_root / "pyproject.toml" 131 | 132 | if path in black_config: 133 | # Already loaded 134 | LOG.debug("flake8-black: %s using pre-loaded %s", self.filename, path) 135 | return black_config[path] 136 | elif path.is_file(): 137 | # Use this pyproject.toml for this python file, 138 | # (unless configured with global override config) 139 | # This should be thread safe - does not matter even if 140 | # two workers load and cache this file at the same time 141 | black_config[path] = load_black_mode(path) 142 | LOG.debug("flake8-black: %s using newly loaded %s", self.filename, path) 143 | return black_config[path] 144 | else: 145 | # No project specific file, use default 146 | LOG.debug("flake8-black: %s using defaults", self.filename) 147 | return black_config[None] 148 | 149 | @classmethod 150 | def add_options(cls, parser): 151 | """Add black-config options.""" 152 | parser.add_option( 153 | "--black-config", 154 | metavar="TOML_FILENAME", 155 | default=None, 156 | action="store", 157 | # type="string", <- breaks using None as a sentinel 158 | # normalize_paths=True, <- broken and breaks None as a sentinel 159 | # https://gitlab.com/pycqa/flake8/issues/562 160 | # https://gitlab.com/pycqa/flake8/merge_requests/337 161 | parse_from_config=True, 162 | help="Path to black TOML configuration file (overrides the " 163 | "default 'pyproject.toml' detection; use empty string '' to mean " 164 | "ignore all 'pyproject.toml' files).", 165 | ) 166 | 167 | @classmethod 168 | def parse_options(cls, optmanager, options, extra_args): 169 | """Parse black-config options.""" 170 | # We have one and only one flake8 plugin configuration 171 | if options.black_config is None: 172 | LOG.info("flake8-black: No black configuration set") 173 | cls.override_config = None 174 | return 175 | elif not options.black_config: 176 | LOG.info("flake8-black: Explicitly using no black configuration file") 177 | cls.override_config = black_config[None] # explicitly use defaults 178 | return 179 | 180 | # Validate the path setting - handling relative paths ourselves, 181 | # see https://gitlab.com/pycqa/flake8/issues/562 182 | black_config_path = Path(options.black_config) 183 | if options.config: 184 | # Assume black config path was via flake8 config file 185 | base_path = Path(path.dirname(path.abspath(options.config))) 186 | black_config_path = base_path / black_config_path 187 | if not black_config_path.is_file(): 188 | # Want flake8 to abort, see: 189 | # https://gitlab.com/pycqa/flake8/issues/559 190 | raise ValueError( 191 | "Plugin flake8-black could not find specified black config file: " 192 | "--black-config %s" % black_config_path 193 | ) 194 | 195 | # Now load the TOML file, and the black section within it 196 | # This configuration is to override any local pyproject.toml 197 | try: 198 | cls.override_config = black_config[black_config_path] = load_black_mode( 199 | black_config_path 200 | ) 201 | except BadBlackConfig: 202 | # Could raise BLK997, but view this as an abort condition 203 | raise ValueError( 204 | "Plugin flake8-black could not parse specified black config file: " 205 | "--black-config %s" % black_config_path 206 | ) 207 | 208 | def run(self): 209 | """Use black to check code style.""" 210 | msg = None 211 | line = 0 212 | col = 0 213 | 214 | try: 215 | if self.filename in self.STDIN_NAMES: 216 | self.filename = "stdin" 217 | raw_source = stdin_utils.stdin_get_value().encode("utf-8") 218 | else: 219 | with open(self.filename, "rb") as buf: 220 | raw_source = buf.read() 221 | except Exception as e: 222 | raw_source = "" 223 | msg = "900 Failed to load file: %s" % e 224 | 225 | if not raw_source and not msg: 226 | # Empty file (good) 227 | return 228 | elif raw_source: 229 | # Call black... 230 | try: 231 | file_mode = self._file_mode 232 | file_mode.is_pyi = self.filename and self.filename.endswith(".pyi") 233 | source, _, _ = decode_bytes_wrapper(raw_source, file_mode) 234 | new_code = black.format_file_contents( 235 | source, mode=file_mode, fast=False 236 | ) 237 | except black.NothingChanged: 238 | return 239 | except black.InvalidInput: 240 | msg = "901 Invalid input." 241 | except (BadBlackConfig, tomllib.TOMLDecodeError): 242 | # Seems in black 25.9.0 the TOMLDecodeError is triggered while 243 | # finding project root, not trivial to get the path 244 | msg = "997 Invalid TOML file" 245 | except Exception as err: 246 | msg = "999 Unexpected exception: %r" % err 247 | else: 248 | assert ( 249 | new_code != source 250 | ), "Black made changes without raising NothingChanged" 251 | line, col = find_diff_start(source, new_code) 252 | line += 1 # Strange as col seems to be zero based? 253 | msg = "100 Black would make changes." 254 | # If we don't know the line or column numbers, leaving as zero. 255 | yield line, col, black_prefix + msg, type(self) 256 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | flake8-black 2 | ============ 3 | 4 | .. image:: https://img.shields.io/pypi/v/flake8-black.svg 5 | :alt: Released on the Python Package Index (PyPI) 6 | :target: https://pypi.org/project/flake8-black/ 7 | .. image:: https://img.shields.io/conda/vn/conda-forge/flake8-black.svg 8 | :alt: Released on Conda 9 | :target: https://anaconda.org/conda-forge/flake8-black 10 | .. image:: https://results.pre-commit.ci/badge/github/peterjc/flake8-black/master.svg 11 | :target: https://results.pre-commit.ci/latest/github/peterjc/flake8-black/master 12 | :alt: pre-commit.ci status 13 | .. image:: https://img.shields.io/github/actions/workflow/status/peterjc/flake8-black/test.yml?logo=github-actions 14 | :alt: GitHub workflow status 15 | :target: https://github.com/peterjc/flake8-black/actions 16 | .. image:: https://img.shields.io/pypi/dm/flake8-black.svg 17 | :alt: PyPI downloads 18 | :target: https://pypistats.org/packages/flake8-black 19 | .. image:: https://img.shields.io/badge/code%20style-black-000000.svg 20 | :alt: Code style: black 21 | :target: https://github.com/python/black 22 | 23 | Introduction 24 | ------------ 25 | 26 | This is an MIT licensed `flake8 `_ plugin 27 | for validating Python code style with the command line code formatting tool 28 | `black `_. It is available to install from 29 | the `Python Package Index (PyPI) `_. 30 | 31 | Black, *"The Uncompromising Code Formatter"*, is normally run to edit your 32 | Python code in place to match their coding style, a strict subset of the 33 | `PEP 8 style guide `_. 34 | 35 | The point of this plugin is to be able to run ``black --check ...`` from 36 | within the ``flake8`` plugin ecosystem. You might use this via a ``git`` 37 | pre-commit hook, or as part of your continuous integration testing. 38 | 39 | If you are using `pre-commit `_ configure it to call 40 | black and/or flake8 directly - you do not need flake8-black at all. 41 | 42 | Flake8 Validation codes 43 | ----------------------- 44 | 45 | Early versions of flake8 assumed a single character prefix for the validation 46 | codes, which became problematic with collisions in the plugin ecosystem. Since 47 | v3.0, flake8 has supported longer prefixes, therefore this plugin uses ``BLK`` 48 | as its prefix. 49 | 50 | ====== ======================================================================= 51 | Code Description (*and notes*) 52 | ------ ----------------------------------------------------------------------- 53 | BLK100 Black would make changes. 54 | BLK9## Internal error (*various, listed below*): 55 | BLK900 Failed to load file: ... 56 | BLK901 Invalid input. 57 | BLK997 Invalid TOML file: ... 58 | BLK998 Could not access flake8 line length setting (*no longer used*). 59 | BLK999 Unexpected exception. 60 | ====== ======================================================================= 61 | 62 | Note that if your Python code has a syntax error, ``black --check ...`` would 63 | report this as an error. Likewise ``flake8 ...`` will by default report the 64 | syntax error, but importantly it does not seem to then call the plugins, so 65 | you will *not* get an additional ``BLK`` error. 66 | 67 | 68 | Installation 69 | ------------ 70 | 71 | Python 3.9 or later is required, but ``black`` can be used on Python code 72 | written for older versions of Python. 73 | 74 | You can install ``flake8-black`` using ``pip``, which should install ``flake8`` 75 | and ``black`` as well if not already present:: 76 | 77 | $ pip install flake8-black 78 | 79 | Alternatively, if you are using the Anaconda packaging system, the following 80 | command will install the plugin with its dependencies:: 81 | 82 | $ conda install -c conda-forge flake8-black 83 | 84 | The new validator should be automatically included when using ``flake8`` which 85 | may now report additional validation codes starting with ``BLK`` (as defined 86 | above). For example:: 87 | 88 | $ flake8 example.py 89 | 90 | You can request only the ``BLK`` codes be shown using:: 91 | 92 | $ flake8 --select BLK example.py 93 | 94 | Python package management 95 | ------------------------- 96 | 97 | For large projects especially, you should consider pinning the exact 98 | version of black you want to use as their updates do sometimes introduce 99 | changes which would show up as new ``BLK100`` violations via flake8. 100 | 101 | You should be able to specify your black version in your conda or pip 102 | requirements or environment, or using using pipenv or poetry etc. 103 | 104 | Configuration 105 | ------------- 106 | 107 | We assume you are familiar with `flake8 configuration 108 | `_ and 109 | `black configuration 110 | `_. 111 | 112 | We recommend using the following settings in your ``flake8`` configuration, 113 | for example in your ``.flake8``, ``setup.cfg``, or ``tox.ini`` file:: 114 | 115 | [flake8] 116 | # Recommend matching the black line length (default 88), 117 | # rather than using the flake8 default of 79: 118 | max-line-length = 88 119 | extend-ignore = 120 | # See https://github.com/PyCQA/pycodestyle/issues/373 121 | E203, 122 | 123 | Note currently ``pycodestyle`` gives false positives on the spaces ``black`` 124 | uses for slices, which ``flake8`` reports as ``E203: whitespace before ':'``. 125 | Until `pyflakes issue 373 `_ 126 | is fixed, and ``flake8`` is updated, we suggest disabling this style check. 127 | 128 | Separately ``pyproject.toml`` is used for ``black`` configuration - if this 129 | file is found, the plugin will look at the following ``black`` settings: 130 | 131 | * ``target_version`` 132 | * ``skip_string_normalization`` 133 | * ``line_length`` 134 | 135 | You can specify a particular path for the ``pyproject.toml`` file (e.g. 136 | global development settings) using ``--black-config FILENAME`` at the 137 | command line, or using ``black-config = FILENAME`` in your ``flake8`` 138 | configuration file. 139 | 140 | Ignoring validation codes 141 | ------------------------- 142 | 143 | Using the flake8 no-quality-assurance pragma comment is not recommended (e.g. 144 | adding ``# noqa: BLK100`` to the first line black would change). Instead use 145 | the black pragma comments ``# fmt: off`` at the start, and ``# fmt: on`` at 146 | the end, of any region of your code which should not be changed. Or, add 147 | ``# fmt: skip`` to single lines. Or, exclude the entire file by name (see 148 | below). 149 | 150 | Ignoring files 151 | -------------- 152 | 153 | The plugin does *NOT* currently consider the ``black`` settings ``include`` 154 | and ``exclude``, so if you have certain Python files which you do not use 155 | with ``black`` and have told it to ignore, you will *also* need to tell 156 | ``flake8`` to ignore them (e.g. using ``exclude`` or ``per-file-ignores``). 157 | 158 | 159 | Version History 160 | --------------- 161 | 162 | ======= ============ =========================================================== 163 | Version Release date Changes 164 | ------- ------------ ----------------------------------------------------------- 165 | v0.4.0 *Pending* - Now tested on Python 3.9 through 3.13, and black 23.9.1 166 | (as before) and black 25.9.0 (current latest). 167 | - Support the API change in ``decode_bytes`` in black 25.9.0. 168 | - Error ``BLK997`` does not have the bad TOML filename. 169 | v0.3.7 2025-09-19 - Now tested on Python 3.8 though 3.13. 170 | - Using ``black.Mode`` class name in place of legacy alias 171 | ``black.FileMode`` from beta release era. 172 | - Does not work on black 25.9.0 onwards. 173 | v0.3.6 2022-12-13 - Use standard library ``tomllib`` on Python 3.11 onwards, 174 | contribution from 175 | `Ganden Schaffner `_. 176 | v0.3.5 2022-11-21 - Fix regression clashing with ``flake8-rst-docstrings``. 177 | v0.3.4 2022-11-17 - Replaces ``setup.py`` with ``pyproject.toml`` for build. 178 | v0.3.3 2022-05-16 - Cope with line-length as string in pyproject.toml config. 179 | v0.3.2 2022-02-25 - Use ``tomli`` library to match black, contribution from 180 | `Brian Helba `_. 181 | - Adopted GitHub Actions to replace TravisCI testing. 182 | - Python 3.7 or later required. 183 | v0.3.0 2022-02-25 - Requires black v22.1.0 (first non-beta release) or later. 184 | - Support options "preview", "skip-magic-trailing-comma" 185 | in the black TOML file, contribution from 186 | `Ferdy `_. 187 | v0.2.4 2022-01-30 - Support black v22.1.0 which changed a function call, 188 | contribution from 189 | `Raffaele Salmaso `_. 190 | v0.2.3 2021-07-16 - Made ``toml`` dependency explicit in ``setup.py``. 191 | v0.2.2 2021-07-16 - Declared ``toml`` dependency (for black 21.7b0). 192 | v0.2.1 2020-07-25 - Detect ``*.pyi`` files via extension. 193 | v0.2.0 2020-05-20 - Minimum requirement on black 19.3b0 or later is now 194 | implicit. This is a workaround for `pipenv issue 3928 195 | `_. Upgrade 196 | black if running flake8 gives an error like this: 197 | ``Flake8 failed to load plugin "BLK" due to __call__() 198 | got an unexpected keyword argument 'target_versions'.`` 199 | v0.1.2 2020-05-18 - Removed test broken by flake8 v3.8 change to resolve 200 | configuration files relative to current directory. 201 | v0.1.1 2019-08-26 - Option to use a (global) black configuration file, 202 | contribution from 203 | `Tomasz Grining `_. 204 | - New ``BLK997`` if can't parse ``pyproject.toml`` file. 205 | - Logs configuration files, use ``-v`` or ``--verbose``. 206 | - Fixed flake8 "builtins" parameter warning. 207 | - Now requires black 19.3b0 or later. 208 | v0.1.0 2019-06-03 - Uses main black settings from ``pyproject.toml``, 209 | contribution from `Alex `_. 210 | - WARNING: Now ignores flake8 ``max-line-length`` setting. 211 | v0.0.4 2019-03-15 - Supports black 19.3b0 which changed a function call. 212 | v0.0.3 2019-02-21 - Bug fix when ``W292 no newline at end of file`` applies, 213 | contribution from 214 | `Sapphire Becker `_. 215 | v0.0.2 2019-02-15 - Document syntax error behaviour (no BLK error reported). 216 | v0.0.1 2019-01-10 - Initial public release. 217 | - Passes flake8 ``max-line-length`` setting to black. 218 | ======= ============ =========================================================== 219 | 220 | 221 | Developers 222 | ---------- 223 | 224 | This plugin is on GitHub at https://github.com/peterjc/flake8-black 225 | 226 | Developers may install the plugin from the git repository with optional build 227 | dependencies:: 228 | 229 | $ pip install -e .[develop] 230 | 231 | To make a new release once tested locally and online:: 232 | 233 | $ git tag vX.Y.Z 234 | $ python -m build 235 | $ git push origin master --tags 236 | $ twine upload dist/flake8?black-X.Y.Z* 237 | 238 | The PyPI upload should trigger an automated pull request updating the 239 | `flake8-black conda-forge recipe 240 | `_. 241 | --------------------------------------------------------------------------------