├── tests ├── fixtures │ ├── empty_folder │ │ └── .gitkeep │ ├── empty_pyproject │ │ └── pyproject.toml │ ├── config_mixed │ │ ├── tox.ini │ │ ├── .flake8 │ │ ├── setup.cfg │ │ ├── module.py │ │ └── pyproject.toml │ ├── empty_tool_section │ │ └── pyproject.toml │ ├── config_toml │ │ ├── module.py │ │ └── flake8.toml │ ├── config_tox │ │ ├── module.py │ │ └── tox.ini │ ├── config_flake8 │ │ ├── module.py │ │ └── .flake8 │ ├── config_pyproject │ │ ├── module.py │ │ └── pyproject.toml │ └── config_setup │ │ ├── module.py │ │ └── setup.cfg ├── ReadMe.md └── test_integration.py ├── flake8p ├── __main__.py ├── __init__.py ├── meta.py └── hook.py ├── tools ├── run_tests.py ├── measure_coverage.py ├── build_wheel.py ├── clean_repo.py └── ReadMe.md ├── .gitignore ├── .github ├── dependabot.yml └── workflows │ ├── scheduled.yml │ ├── coverage.yml │ ├── publish.yml │ └── commit.yml ├── license.txt ├── PyPI.md ├── pyproject.toml └── ReadMe.md /tests/fixtures/empty_folder/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/empty_pyproject/pyproject.toml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/config_mixed/tox.ini: -------------------------------------------------------------------------------- 1 | [flake8] 2 | count = false 3 | -------------------------------------------------------------------------------- /tests/fixtures/config_mixed/.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | per-file-ignores = 3 | -------------------------------------------------------------------------------- /tests/fixtures/config_mixed/setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 80 3 | -------------------------------------------------------------------------------- /tests/fixtures/empty_tool_section/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.other] 2 | key = 'value' 3 | -------------------------------------------------------------------------------- /flake8p/__main__.py: -------------------------------------------------------------------------------- 1 | """Calls main entry point if run via `python -m flake8p`.""" 2 | 3 | from . import main 4 | 5 | if __name__ == '__main__': 6 | raise SystemExit(main()) 7 | -------------------------------------------------------------------------------- /tests/fixtures/config_toml/module.py: -------------------------------------------------------------------------------- 1 | """This line is 77 characters long, less than the default max-line-length.""" 2 | 3 | import foo 4 | 5 | missing_space = (None,None) 6 | multiple_spaces = (None, None) 7 | -------------------------------------------------------------------------------- /tests/fixtures/config_tox/module.py: -------------------------------------------------------------------------------- 1 | """This line is 77 characters long, less than the default max-line-length.""" 2 | 3 | import foo 4 | 5 | missing_space = (None,None) 6 | multiple_spaces = (None, None) 7 | -------------------------------------------------------------------------------- /tests/fixtures/config_flake8/module.py: -------------------------------------------------------------------------------- 1 | """This line is 77 characters long, less than the default max-line-length.""" 2 | 3 | import foo 4 | 5 | missing_space = (None,None) 6 | multiple_spaces = (None, None) 7 | -------------------------------------------------------------------------------- /tests/fixtures/config_mixed/module.py: -------------------------------------------------------------------------------- 1 | """This line is 77 characters long, less than the default max-line-length.""" 2 | 3 | import foo 4 | 5 | missing_space = (None,None) 6 | multiple_spaces = (None, None) 7 | -------------------------------------------------------------------------------- /tests/fixtures/config_pyproject/module.py: -------------------------------------------------------------------------------- 1 | """This line is 77 characters long, less than the default max-line-length.""" 2 | 3 | import foo 4 | 5 | missing_space = (None,None) 6 | multiple_spaces = (None, None) 7 | -------------------------------------------------------------------------------- /tests/fixtures/config_setup/module.py: -------------------------------------------------------------------------------- 1 | """This line is 77 characters long, less than the default max-line-length.""" 2 | 3 | import foo 4 | 5 | missing_space = (None,None) 6 | multiple_spaces = (None, None) 7 | -------------------------------------------------------------------------------- /tools/run_tests.py: -------------------------------------------------------------------------------- 1 | """Runs the test suite.""" 2 | 3 | from subprocess import run 4 | from pathlib import Path 5 | 6 | 7 | root = Path(__file__).resolve().parent.parent 8 | 9 | run(['pytest'], cwd=root) 10 | -------------------------------------------------------------------------------- /flake8p/__init__.py: -------------------------------------------------------------------------------- 1 | # The imports here define the public interface of the package. 2 | 3 | from .meta import version as __version__ 4 | from .meta import synopsis as __doc__ 5 | from .hook import main 6 | from .hook import Plugin 7 | -------------------------------------------------------------------------------- /tests/ReadMe.md: -------------------------------------------------------------------------------- 1 | ## Test suite 2 | 3 | The scripts here, along with the fixtures, constitute the test suite. 4 | Run them with `pytest` from the root folder. Or use the scripts `run_tests.py` 5 | and `measure_coverage.py` from the `tools` folder. 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Build and test artifacts 2 | __pycache__/ 3 | .pytest_cache/ 4 | /.venv/ 5 | /venv/ 6 | /build/ 7 | /dist/ 8 | 9 | # Editor configs 10 | /.vscode/ 11 | /.idea/ 12 | .spyproject 13 | 14 | # OS cruft 15 | Thumbs.db 16 | .DS_Store 17 | -------------------------------------------------------------------------------- /flake8p/meta.py: -------------------------------------------------------------------------------- 1 | """Meta information about the package.""" 2 | 3 | title = 'Flake8-pyproject' 4 | synopsis = 'Flake8 plug-in loading the configuration from pyproject.toml' 5 | version = '1.2.4' 6 | author = 'John Hennig' 7 | license = 'MIT' 8 | -------------------------------------------------------------------------------- /tests/fixtures/config_tox/tox.ini: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = 3 | # Missing whitespace after ',', ';', or ':'. 4 | E231, 5 | # Multiple spaces after ','. 6 | E241, 7 | per-file-ignores = 8 | module.py:F401 9 | max-line-length = 70 10 | count = true 11 | -------------------------------------------------------------------------------- /tests/fixtures/config_flake8/.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = 3 | # Missing whitespace after ',', ';', or ':'. 4 | E231, 5 | # Multiple spaces after ','. 6 | E241, 7 | per-file-ignores = 8 | module.py:F401 9 | max-line-length = 70 10 | count = true 11 | -------------------------------------------------------------------------------- /tests/fixtures/config_mixed/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.flake8] 2 | ignore = [ 3 | 'E231', # Missing whitespace after ',', ';', or ':'. 4 | 'E241', # Multiple spaces after ','. 5 | ] 6 | per-file-ignores = ['module.py:F401'] 7 | max-line-length = 70 8 | count = true 9 | -------------------------------------------------------------------------------- /tests/fixtures/config_setup/setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = 3 | # Missing whitespace after ',', ';', or ':'. 4 | E231, 5 | # Multiple spaces after ','. 6 | E241, 7 | per-file-ignores = 8 | module.py:F401 9 | max-line-length = 70 10 | count = true 11 | -------------------------------------------------------------------------------- /tests/fixtures/config_toml/flake8.toml: -------------------------------------------------------------------------------- 1 | [tool.flake8] 2 | ignore = [ 3 | 'E231', # Missing whitespace after ',', ';', or ':'. 4 | 'E241', # Multiple spaces after ','. 5 | ] 6 | per-file-ignores = ['module.py:F401'] 7 | max-line-length = 70 8 | count = true 9 | -------------------------------------------------------------------------------- /tests/fixtures/config_pyproject/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.flake8] 2 | ignore = [ 3 | 'E231', # Missing whitespace after ',', ';', or ':'. 4 | 'E241', # Multiple spaces after ','. 5 | ] 6 | per-file-ignores = ['module.py:F401'] 7 | max-line-length = 70 8 | count = true 9 | -------------------------------------------------------------------------------- /tools/measure_coverage.py: -------------------------------------------------------------------------------- 1 | """Measures code coverage by test suite.""" 2 | 3 | from subprocess import run 4 | from pathlib import Path 5 | from sys import executable as python 6 | 7 | 8 | print('Running test suite.') 9 | root = Path(__file__).resolve().parent.parent 10 | run( 11 | [ 12 | python, '-m', 'pytest', 13 | '--cov', 14 | '--cov-report=html', 15 | '--cov-report=xml', 16 | '--cov-report=term' 17 | ], 18 | cwd=root, 19 | ) 20 | -------------------------------------------------------------------------------- /tools/build_wheel.py: -------------------------------------------------------------------------------- 1 | """Builds the distribution wheel.""" 2 | 3 | from subprocess import run 4 | from pathlib import Path 5 | from shutil import rmtree 6 | 7 | 8 | root = Path(__file__).resolve().parent.parent 9 | 10 | process = run(['flit', 'build', '--format', 'wheel'], cwd=root) 11 | if process.returncode: 12 | raise RuntimeError('Error while building wheel.') 13 | 14 | source = root/'dist' 15 | target = root/'build'/'wheel' 16 | if target.exists(): 17 | rmtree(target) 18 | source.rename(target) 19 | -------------------------------------------------------------------------------- /tools/clean_repo.py: -------------------------------------------------------------------------------- 1 | """Deletes build and test artifacts.""" 2 | 3 | from pathlib import Path 4 | from shutil import rmtree 5 | 6 | 7 | root = Path(__file__).resolve().parent.parent 8 | 9 | folders = [ 10 | root/'build', 11 | root/'dist', 12 | ] 13 | folder_names = [ 14 | '__pycache__', 15 | '.pytest_cache', 16 | ] 17 | 18 | for folder_name in folder_names: 19 | for folder in root.rglob(folder_name): 20 | folders.append(folder) 21 | 22 | for folder in folders: 23 | if folder.is_dir(): 24 | rmtree(folder, ignore_errors=True) 25 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Keep GitHub Actions up to date with GitHub's Dependabot... 2 | # https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot 3 | # https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#package-ecosystem 4 | version: 2 5 | updates: 6 | - package-ecosystem: github-actions 7 | directory: / 8 | groups: 9 | github-actions: 10 | patterns: 11 | - "*" # Group all Actions updates into a single larger pull request 12 | schedule: 13 | interval: weekly 14 | -------------------------------------------------------------------------------- /tools/ReadMe.md: -------------------------------------------------------------------------------- 1 | ## Developer tools 2 | 3 | These are simple helper scripts to run the various dev tools, such as the test 4 | suite or to build the distribution wheel. See the doc-strings of the individual 5 | scripts for details. 6 | 7 | For local development, install the package in editable mode inside a dedicated 8 | virtual environment with `pip install --editable .[dev]`. 9 | 10 | 11 | ### Releasing a new version 12 | 13 | - Bump version number in `meta.py`. 14 | - Add dedicated commit for the version bump. 15 | - Publish to PyPI via GitHub Action. 16 | - Create release on GitHub, tag it (like `v1.2.4`), add release notes. 17 | -------------------------------------------------------------------------------- /.github/workflows/scheduled.yml: -------------------------------------------------------------------------------- 1 | # Regular tests against the current Flake8 main branch. 2 | name: Test weekly 3 | 4 | on: 5 | schedule: 6 | # Every Saturday at 11:17 UTC. 7 | - cron: '17 11 * * 6' 8 | 9 | jobs: 10 | 11 | flake8_main: 12 | 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Check out code. 17 | uses: actions/checkout@v6 18 | 19 | - name: Set up latest stable Python. 20 | uses: actions/setup-python@v6 21 | with: 22 | python-version: '3.x' 23 | 24 | - name: Install current Flake8 main branch. 25 | run: pip install git+https://github.com/PyCQA/flake8.git 26 | 27 | - name: Install project. 28 | run: pip install .[dev] 29 | 30 | - name: Run tests. 31 | run: pytest 32 | -------------------------------------------------------------------------------- /.github/workflows/coverage.yml: -------------------------------------------------------------------------------- 1 | # Report code coverage to Codecov. 2 | name: Report coverage 3 | 4 | on: [push, workflow_dispatch] 5 | 6 | jobs: 7 | 8 | coverage: 9 | 10 | runs-on: ubuntu-latest 11 | 12 | strategy: 13 | matrix: 14 | python: ['3.10', '3.14'] 15 | 16 | steps: 17 | - name: Check out code. 18 | uses: actions/checkout@v6 19 | 20 | - name: Set up Python. 21 | uses: actions/setup-python@v6 22 | with: 23 | python-version: ${{ matrix.python }} 24 | allow-prereleases: true 25 | 26 | - name: Install project. 27 | run: pip install --editable .[dev] 28 | 29 | - name: Measure code coverage. 30 | run: pytest --cov --cov-report=xml 31 | 32 | - name: Upload coverage report. 33 | uses: codecov/codecov-action@v5 34 | with: 35 | token: ${{ secrets.CODECOV_TOKEN }} 36 | fail_ci_if_error: true 37 | files: ./build/coverage/coverage.xml 38 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 John Hennig 4 | 5 | Permission is hereby granted, free of charge, to any person 6 | obtaining a copy of this software and associated documentation 7 | files (the "Software"), to deal in the Software without restriction, 8 | including without limitation the rights to use, copy, modify, merge, 9 | publish, distribute, sublicense, and/or sell copies of the Software, 10 | and to permit persons to whom the Software is furnished to do so, 11 | subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | # Publish new release on PyPI. 2 | name: Publish release 3 | 4 | on: [workflow_dispatch] 5 | 6 | jobs: 7 | 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | 12 | - name: Check out code. 13 | uses: actions/checkout@v6 14 | 15 | - name: Install Python. 16 | uses: actions/setup-python@v6 17 | with: 18 | python-version: '3.14' 19 | 20 | - name: Install project. 21 | run: pip install --editable .[dev] 22 | 23 | - name: Build wheel. 24 | run: flit build --format wheel 25 | 26 | - name: Store wheel. 27 | uses: actions/upload-artifact@v6 28 | with: 29 | name: wheel 30 | path: dist 31 | 32 | 33 | publish: 34 | needs: 35 | - build 36 | runs-on: ubuntu-latest 37 | environment: 38 | name: PyPI 39 | url: https://pypi.org/p/Flake8-pyproject 40 | permissions: 41 | id-token: write 42 | steps: 43 | 44 | - name: Download wheel. 45 | uses: actions/download-artifact@v7 46 | with: 47 | name: wheel 48 | path: dist 49 | 50 | - name: Publish to PyPI. 51 | uses: pypa/gh-action-pypi-publish@release/v1 52 | with: 53 | packages-dir: dist 54 | -------------------------------------------------------------------------------- /.github/workflows/commit.yml: -------------------------------------------------------------------------------- 1 | # Test commit on all supported platforms and Python versions. 2 | name: Test commit 3 | 4 | on: [push, pull_request, workflow_dispatch] 5 | 6 | jobs: 7 | 8 | # Test on all three platforms. 9 | platform: 10 | 11 | strategy: 12 | matrix: 13 | os: [windows-latest, ubuntu-latest, macos-latest] 14 | 15 | runs-on: ${{ matrix.os }} 16 | 17 | steps: 18 | - name: Check out code. 19 | uses: actions/checkout@v6 20 | 21 | - name: Set up Python. 22 | uses: actions/setup-python@v6 23 | with: 24 | python-version: 3.x 25 | 26 | - name: Install project. 27 | run: pip install .[dev] 28 | 29 | - name: Run tests. 30 | run: pytest 31 | 32 | 33 | # Test on all supported Python versions. 34 | Python: 35 | 36 | strategy: 37 | matrix: 38 | python: ['3.10', '3.11', '3.12', '3.13', '3.14'] 39 | 40 | runs-on: ubuntu-latest 41 | 42 | steps: 43 | - name: Check out code. 44 | uses: actions/checkout@v6 45 | 46 | - name: Set up Python ${{ matrix.python }}. 47 | uses: actions/setup-python@v6 48 | with: 49 | python-version: ${{ matrix.python }} 50 | allow-prereleases: true 51 | 52 | - name: Install package. 53 | run: pip install .[dev] 54 | 55 | - name: Run tests. 56 | run: pytest 57 | 58 | -------------------------------------------------------------------------------- /PyPI.md: -------------------------------------------------------------------------------- 1 | # Flake8-pyproject 2 | *Flake8 plug-in loading the configuration from `pyproject.toml`* 3 | 4 | [Flake8] cannot be configured via `pyproject.toml`, even though 5 | virtually all other Python dev tools have adopted it as the central 6 | location for project configuration. The discussion of the original 7 | proposal ([#234]) was closed as "too heated", subsequent feature 8 | and pull requests were marked as "spam" ([#1332], [#1421], [#1431], 9 | [#1447], [#1501]). 10 | 11 | Flake8-pyproject also has bad manners and force-feeds Flake8 the 12 | spam it so despises. It registers itself as a Flake8 plug-in to 13 | seamlessly load the configuration from `pyproject.toml` when you 14 | run the `flake8` command. 15 | 16 | [Flake8]: https://github.com/PyCQA/flake8 17 | [#234]: https://github.com/PyCQA/flake8/issues/234 18 | [#1332]: https://github.com/PyCQA/flake8/pull/1332 19 | [#1421]: https://github.com/PyCQA/flake8/issues/1421 20 | [#1431]: https://github.com/PyCQA/flake8/issues/1431 21 | [#1447]: https://github.com/PyCQA/flake8/issues/1447 22 | [#1501]: https://github.com/PyCQA/flake8/issues/1501 23 | 24 | 25 | ## Usage 26 | 27 | Say your Flake8 configuration in `.flake8` (or in `tox.ini`, or 28 | `setup.cfg`) is this: 29 | ```ini 30 | [flake8] 31 | ignore = E231, E241 32 | per-file-ignores = 33 | __init__.py:F401 34 | max-line-length = 88 35 | count = true 36 | ``` 37 | 38 | Copy that `[flake8]` section to `pyproject.toml`, rename it as 39 | `[tool.flake8]`, and convert the key–value pairs to the [TOML format]: 40 | ```toml 41 | [tool.flake8] 42 | ignore = ['E231', 'E241'] 43 | per-file-ignores = [ 44 | '__init__.py:F401', 45 | ] 46 | max-line-length = 88 47 | count = true 48 | ``` 49 | 50 | Then run `flake8` in the project root folder, where `pyproject.toml` 51 | is located. 52 | 53 | In case your TOML-based configuration is contained in a different 54 | folder, or the file has a different name, specify the location with 55 | the `--toml-config` command-line option. 56 | 57 | For compatibility with earlier versions of this package, and perhaps 58 | extra reliability in terms of possible future breakage of the plug-in 59 | hook, the package also provides a `flake8p` command that could be 60 | called alternatively to lint the code. 61 | 62 | [TOML format]: https://toml.io 63 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | # Project information: PyPI and Pip 2 | 3 | [project] 4 | name = 'Flake8-pyproject' 5 | authors = [{name='John Hennig'}] 6 | dynamic = ['version', 'description'] 7 | keywords = ['Flake8', 'pyproject.toml'] 8 | classifiers = [ 9 | 'Development Status :: 5 - Production/Stable', 10 | 'Programming Language :: Python', 11 | 'Topic :: Software Development :: Quality Assurance', 12 | 'Intended Audience :: Developers', 13 | 'Environment :: Console', 14 | 'Framework :: Flake8', 15 | 'License :: OSI Approved :: MIT License', 16 | ] 17 | license = {file='license.txt'} 18 | readme = 'PyPI.md' 19 | 20 | requires-python = '>= 3.6' 21 | dependencies = [ 22 | 'Flake8 >= 5', 23 | 'TOMLi ; python_version < "3.11"', 24 | 'TOMLi < 2 ; python_version < "3.7"', 25 | ] 26 | 27 | [project.optional-dependencies] 28 | dev = [ 29 | 'Flit >= 3.4', 30 | 'pyTest >= 7', 31 | 'pyTest-cov >= 7; python_version >= "3.10"', 32 | ] 33 | 34 | [project.scripts] 35 | flake8p = 'flake8p:main' 36 | 37 | [project.entry-points.'flake8.report'] 38 | Flake8-pyproject = 'flake8p:Plugin' 39 | 40 | [project.urls] 41 | Source = 'https://github.com/john-hen/Flake8-pyproject' 42 | Releases = 'https://github.com/john-hen/Flake8-pyproject/releases' 43 | 44 | 45 | # Wheel builder: Flit 46 | 47 | [build-system] 48 | requires = ['flit_core >= 3.4'] 49 | build-backend = 'flit_core.buildapi' 50 | 51 | [tool.flit.module] 52 | name = 'flake8p' 53 | 54 | 55 | # Code linter: Flake8 56 | 57 | [tool.flake8] 58 | exclude = ['tests/fixtures'] 59 | ignore = [ 60 | 'E221', # Multiple spaces before operator. 61 | 'E226', # Missing whitespace around arithmetic operator. 62 | 'E272', # Multiple spaces before keyword. 63 | ] 64 | per-file-ignores = [ 65 | '__init__.py:F401', # Module imported but unused. 66 | ] 67 | 68 | 69 | # Test runner: pyTest 70 | 71 | [tool.pytest.ini_options] 72 | testpaths = ['tests'] 73 | addopts = ['--verbose'] 74 | console_output_style = 'count' 75 | 76 | [tool.coverage.run] 77 | source = ['flake8p'] 78 | data_file = 'build/coverage/.coverage' 79 | patch = ['subprocess'] 80 | 81 | [tool.coverage.html] 82 | directory = 'build/coverage' 83 | 84 | [tool.coverage.xml] 85 | output = 'build/coverage/coverage.xml' 86 | -------------------------------------------------------------------------------- /tests/test_integration.py: -------------------------------------------------------------------------------- 1 | """Integration tests for the package.""" 2 | 3 | from pathlib import Path 4 | from subprocess import run, PIPE, STDOUT 5 | from sys import executable as python 6 | from pytest import mark 7 | 8 | 9 | expected = r""" 10 | module.py:1:71: E501 line too long (77 > 70 characters) 11 | module.py:5:14: E221 multiple spaces before operator 12 | 2 13 | """.strip() 14 | 15 | 16 | def capture(command, folder): 17 | if isinstance(folder, str): 18 | folder = Path(__file__).parent/'fixtures'/folder 19 | process = run(command, stdout=PIPE, stderr=STDOUT, 20 | universal_newlines=True, cwd=folder) 21 | # From Python 3.7 on, use `text=True` instead of `universal newlines`. 22 | return process.stdout.strip() 23 | 24 | 25 | @mark.parametrize('command', ['flake8', 'flake8p']) 26 | def test_config_pyproject(command): 27 | output = capture([command, 'module.py'], 'config_pyproject') 28 | assert output == expected 29 | 30 | 31 | @mark.parametrize('command', ['flake8', 'flake8p']) 32 | def test_config_flake8(command): 33 | output = capture([command, 'module.py'], 'config_flake8') 34 | assert output == expected 35 | 36 | 37 | @mark.parametrize('command', ['flake8', 'flake8p']) 38 | def test_config_setup(command): 39 | output = capture([command, 'module.py'], 'config_setup') 40 | assert output == expected 41 | 42 | 43 | @mark.parametrize('command', ['flake8', 'flake8p']) 44 | def test_config_tox(command): 45 | output = capture([command, 'module.py'], 'config_tox') 46 | assert output == expected 47 | 48 | 49 | @mark.parametrize('command', ['flake8', 'flake8p']) 50 | def test_config_mixed(command): 51 | output = capture([command, 'module.py'], 'config_mixed') 52 | assert output == expected 53 | 54 | 55 | @mark.parametrize('command', ['flake8', 'flake8p']) 56 | def test_config_toml(command): 57 | here = Path(__file__).parent 58 | file = here/'fixtures'/'config_toml'/'flake8.toml' 59 | output = capture([command, f'--toml-config={file.name}', 'module.py'], 60 | 'config_toml') 61 | assert output == expected 62 | output = capture([command, f'--toml-config={file.resolve()}', 'module.py'], 63 | 'config_toml') 64 | assert output == expected 65 | file = file.with_name('does_not_exist.toml') 66 | assert not file.exists() 67 | output = capture([command, f'--toml-config={file.name}'], 'config_toml') 68 | assert 'FileNotFoundError' in output 69 | 70 | 71 | @mark.parametrize('command', ['flake8', 'flake8p']) 72 | def test_empty_folder(command): 73 | output = capture([command], 'empty_folder') 74 | assert not output 75 | 76 | 77 | @mark.parametrize('command', ['flake8', 'flake8p']) 78 | def test_empty_pyproject(command): 79 | output = capture([command], 'empty_pyproject') 80 | assert not output 81 | 82 | 83 | @mark.parametrize('command', ['flake8', 'flake8p']) 84 | def test_empty_tool_section(command): 85 | output = capture([command], 'empty_tool_section') 86 | assert not output 87 | 88 | 89 | @mark.parametrize('command', ['flake8', 'flake8p']) 90 | def test_run_main(command): 91 | output = capture([python, '-m', command, 'module.py'], 'config_mixed') 92 | assert output == expected 93 | -------------------------------------------------------------------------------- /ReadMe.md: -------------------------------------------------------------------------------- 1 | # Flake8-pyproject 2 | *Flake8 plug-in loading the configuration from `pyproject.toml`* 3 | 4 | [Flake8] cannot be configured via `pyproject.toml`, even though 5 | virtually all other Python dev tools have adopted it as the central 6 | location for project configuration. The discussion of the original 7 | proposal ([#234]) was closed as "too heated", subsequent feature 8 | and pull requests were marked as "spam" ([#1332], [#1421], [#1431], 9 | [#1447], [#1501]). 10 | 11 | Flake8-pyproject also has bad manners and force-feeds Flake8 the 12 | spam it so despises. It registers itself as a Flake8 plug-in to 13 | seamlessly load the configuration from `pyproject.toml` when you 14 | run the `flake8` command in the same folder. 15 | 16 | [Flake8]: https://github.com/PyCQA/flake8 17 | [#234]: https://github.com/PyCQA/flake8/issues/234 18 | [#1332]: https://github.com/PyCQA/flake8/pull/1332 19 | [#1421]: https://github.com/PyCQA/flake8/issues/1421 20 | [#1431]: https://github.com/PyCQA/flake8/issues/1431 21 | [#1447]: https://github.com/PyCQA/flake8/issues/1447 22 | [#1501]: https://github.com/PyCQA/flake8/issues/1501 23 | 24 | 25 | ## Usage 26 | 27 | Say your Flake8 configuration in `.flake8` (or in `tox.ini`, or 28 | `setup.cfg`) is this: 29 | ```ini 30 | [flake8] 31 | ignore = E231, E241 32 | per-file-ignores = 33 | __init__.py:F401 34 | max-line-length = 88 35 | count = true 36 | ``` 37 | 38 | Copy that `[flake8]` section to `pyproject.toml`, rename it as 39 | `[tool.flake8]`, and convert the key–value pairs to the [TOML format]: 40 | ```toml 41 | [tool.flake8] 42 | ignore = ['E231', 'E241'] 43 | per-file-ignores = [ 44 | '__init__.py:F401', 45 | ] 46 | max-line-length = 88 47 | count = true 48 | ``` 49 | 50 | Then run `flake8` in the project root folder, where `pyproject.toml` 51 | is located. 52 | 53 | In case your TOML-based configuration is contained in a different 54 | folder, or the file has a different name, specify the location with 55 | the `--toml-config` command-line option. 56 | 57 | For compatibility with earlier versions of this package, and perhaps 58 | extra reliability in terms of possible future breakage of the plug-in 59 | hook, the package also provides a `flake8p` command that could be 60 | called alternatively. 61 | 62 | [TOML format]: https://toml.io 63 | 64 | 65 | ## Implementation 66 | 67 | Flake8 uses [`RawConfigParser`] from the standard library to parse its 68 | configuration files, and therefore expects them to have the [INI 69 | format]. 70 | 71 | This library hooks into Flake8's plug-in mechanism to load the 72 | configuration from `pyproject.toml` instead, *if* it finds such a file 73 | in the current folder (working directory). It then creates a 74 | `RawConfigParser` instance, converting from the TOML input format, 75 | and passes it on to Flake8 while discarding configuration options that 76 | would otherwise be sourced from elsewhere. 77 | 78 | As of Python 3.11, a TOML parser is part of the standard library ([PEP 79 | 680]). On older Python installations, we rely on [Tomli]. 80 | 81 | A few very simple integration tests round out the package, making sure 82 | that any one of the possible configuration files are in fact accepted 83 | when `pyproject.toml` isn't found. 84 | 85 | [`RawConfigParser`]: https://docs.python.org/3/library/configparser.html#configparser.RawConfigParser 86 | [INI format]: https://en.wikipedia.org/wiki/INI_file#Format 87 | [Tomli]: https://pypi.org/project/tomli/ 88 | [PEP 680]: https://www.python.org/dev/peps/pep-0680 89 | 90 | 91 | [![release]( 92 | https://img.shields.io/pypi/v/Flake8-pyproject.svg?label=release)]( 93 | https://pypi.python.org/pypi/Flake8-pyproject) 94 | [![coverage]( 95 | https://img.shields.io/codecov/c/github/john-hen/Flake8-pyproject?token=30Gjak3Ksu)]( 96 | https://codecov.io/gh/john-hen/Flake8-pyproject) 97 | -------------------------------------------------------------------------------- /flake8p/hook.py: -------------------------------------------------------------------------------- 1 | """Hooks TOML parser into Flake8.""" 2 | 3 | ######################################## 4 | # Imports # 5 | ######################################## 6 | 7 | from . import meta 8 | import flake8.main.cli 9 | import flake8.options.aggregator 10 | import flake8.options.config 11 | import sys 12 | if sys.version_info >= (3, 11): 13 | import tomllib as toml 14 | else: 15 | import tomli as toml 16 | import configparser 17 | from pathlib import Path 18 | 19 | 20 | ######################################## 21 | # Hook # 22 | ######################################## 23 | 24 | # Remember original Flake8 objects. 25 | flake8_aggregate_options = flake8.options.aggregator.aggregate_options 26 | flake8_parse_config = flake8.options.config.parse_config 27 | 28 | # Global variable pointing to TOML config. 29 | toml_config = Path('pyproject.toml') 30 | 31 | 32 | def aggregate_options(manager, cfg, cfg_dir, argv): 33 | """ 34 | Overrides Flake8's option aggregation. 35 | 36 | If a custom TOML file was specified via the `--toml-config` 37 | command-line option, its value is stored in a global variable for 38 | later consumption in parse_config(). 39 | 40 | Finally, Flake8's `aggregate_options()` is called as usual. 41 | """ 42 | global toml_config 43 | arguments = manager.parser.parse_known_args(argv)[0] 44 | if arguments.toml_config: 45 | toml_config = Path(arguments.toml_config) 46 | if not toml_config.exists(): 47 | raise FileNotFoundError( 48 | f'Plug-in {meta.title} could not find ' 49 | f'custom configuration file "{toml_config}".') 50 | return flake8_aggregate_options(manager, cfg, cfg_dir, argv) 51 | 52 | 53 | def parse_config(option_manager, cfg, cfg_dir): 54 | """ 55 | Overrides Flake8's configuration parsing. 56 | 57 | If we discover `pyproject.toml` in the current folder, we discard 58 | anything that may have been read from whatever other configuration 59 | file and read the `tool.flake8` section in `pyproject.toml` instead. 60 | 61 | If a custom TOML file was specified via the `--toml-config` 62 | command-line option, we read the section from that file instead. 63 | """ 64 | if toml_config.exists(): 65 | with toml_config.open('rb') as stream: 66 | pyproject = toml.load(stream) 67 | if 'tool' in pyproject and 'flake8' in pyproject['tool']: 68 | parser = configparser.RawConfigParser() 69 | section = 'flake8' 70 | parser.add_section(section) 71 | for (key, value) in pyproject['tool']['flake8'].items(): 72 | if isinstance(value, (bool, int, float)): 73 | value = str(value) 74 | parser.set(section, key, value) 75 | (cfg, cfg_dir) = (parser, str(toml_config.resolve().parent)) 76 | 77 | return flake8_parse_config(option_manager, cfg, cfg_dir) 78 | 79 | 80 | ######################################## 81 | # Plug-in # 82 | ######################################## 83 | 84 | class Plugin: 85 | """ 86 | Installs the hook when called via `flake8` itself. 87 | 88 | Also adds the command-line option `--toml-config` to Flake8. 89 | """ 90 | @classmethod 91 | def add_options(cls, parser): 92 | flake8.options.aggregator.aggregate_options = aggregate_options 93 | flake8.options.config.parse_config = parse_config 94 | parser.add_option( 95 | '--toml-config', metavar='TOML_CONFIG', 96 | default=None, action='store', 97 | parse_from_config=True, 98 | help='Path to custom TOML configuration file. May be located in a ' 99 | 'different folder. Overrides the default "pyproject.toml" ' 100 | 'in the current working directory.', 101 | ) 102 | 103 | 104 | ######################################## 105 | # Main # 106 | ######################################## 107 | 108 | # Also call Flake8 when we are called via our own `flake8p` entry point. 109 | # We keep this entry point alive for backward compatibility. And also 110 | # in case the plug-in hook breaks, so users have something to fall back 111 | # on that would be easier to fix in our own code. At this point, however, 112 | # we just call Flake8 directly, which will then load the above hook via 113 | # the plug-in mechanism. 114 | main = flake8.main.cli.main 115 | --------------------------------------------------------------------------------