├── test ├── data │ ├── setup.cfg │ ├── setup_custom.cfg │ ├── pyproject.toml │ └── dummy.py └── test_append_config.py ├── .gitignore ├── pflake8 ├── __main__.py └── __init__.py ├── .github └── workflows │ ├── black.yml │ ├── test.yml │ ├── check.yml │ ├── build.yml │ └── maintenance.py ├── .pre-commit-hooks.yaml ├── pyproject.toml ├── LICENSE └── README.md /test/data/setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 120 3 | -------------------------------------------------------------------------------- /test/data/setup_custom.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | extend-ignore = E203 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | .idea/ 3 | venv/ 4 | 5 | *.pyc 6 | __pycache__/ 7 | -------------------------------------------------------------------------------- /test/data/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.flake8] 2 | extend-ignore = ["E203"] 3 | -------------------------------------------------------------------------------- /pflake8/__main__.py: -------------------------------------------------------------------------------- 1 | from . import main 2 | 3 | if __name__ == '__main__': 4 | raise SystemExit(main()) 5 | -------------------------------------------------------------------------------- /test/data/dummy.py: -------------------------------------------------------------------------------- 1 | d = {"a" : 1} # E203 2 | 3 | 4 | def a_long_name_function_definition(x="this is a test for", y="--max-line-length setting."): # E501 5 | pass 6 | -------------------------------------------------------------------------------- /.github/workflows/black.yml: -------------------------------------------------------------------------------- 1 | name: Style check 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | lint: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | - uses: psf/black@stable 11 | with: 12 | version: "24.3.0" -------------------------------------------------------------------------------- /.pre-commit-hooks.yaml: -------------------------------------------------------------------------------- 1 | - id: pyproject-flake8 2 | name: pyproject-flake8 3 | description: 'pyproject-flake8 (`pflake8`), a monkey patching wrapper to connect flake8 with pyproject.toml configuration' 4 | entry: pflake8 5 | language: python 6 | types: [python] 7 | require_serial: true 8 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] 11 | name: Python ${{ matrix.python-version }} testing 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: actions/setup-python@v5 15 | with: 16 | python-version: ${{ matrix.python-version }} 17 | - name: Install 18 | run: python -m pip install . 19 | - name: Test 20 | run: python -m unittest discover -v -s test 21 | -------------------------------------------------------------------------------- /.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | name: flake8 version check 2 | 3 | on: 4 | schedule: 5 | # run the action always at 1:11 UTC (i.e. 3:11 CEST) 6 | - cron: '11 1 * * *' 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | python-version: ["3.12"] 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: actions/setup-python@v5 17 | with: 18 | python-version: ${{ matrix.python-version }} 19 | - name: Install dependencies 20 | run: python -m pip install requests tomli 21 | - name: Perform flake8 version check 22 | run: python .github/workflows/maintenance.py check -------------------------------------------------------------------------------- /test/test_append_config.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import unittest 3 | import pflake8 4 | 5 | 6 | class TestAppendConfig(unittest.TestCase): 7 | def test_append_config_toml(self): 8 | sys.argv = [ 9 | "pflake8", 10 | "--config", 11 | "./test/data/setup.cfg", 12 | "--append-config", 13 | "./test/data/pyproject.toml", 14 | "./test/data/dummy.py", 15 | ] 16 | 17 | pflake8.main() # OK if this raises no exception. 18 | 19 | def test_append_config_normal(self): 20 | sys.argv = [ 21 | "pflake8", 22 | "--config", 23 | "./test/data/setup.cfg", 24 | "--append-config", 25 | "./test/data/setup_custom.cfg", 26 | "./test/data/dummy.py", 27 | ] 28 | 29 | pflake8.main() # OK if this raises no exception. 30 | 31 | 32 | if __name__ == '__main__': 33 | unittest.main() 34 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build package 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | environment: release 9 | permissions: 10 | # IMPORTANT: this permission is mandatory for trusted publishing 11 | id-token: write 12 | strategy: 13 | matrix: 14 | python-version: ["3.12"] 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: actions/setup-python@v5 18 | with: 19 | python-version: ${{ matrix.python-version }} 20 | - name: Install dependencies 21 | run: python -m pip install --upgrade pip wheel build 22 | - name: Building packages 23 | run: python -m build 24 | - uses: actions/upload-artifact@v4 25 | with: 26 | name: package 27 | path: dist/* 28 | - name: Publish distribution to PyPI 29 | if: startsWith(github.ref, 'refs/tags') 30 | uses: pypa/gh-action-pypi-publish@v1.8.14 31 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["flit_core >=3.2,<4"] 3 | build-backend = "flit_core.buildapi" 4 | 5 | [project] 6 | dynamic = ["version", "description"] 7 | name = "pyproject-flake8" 8 | authors = [{name = "Christian Sachs", email = "sachs.christian@gmail.com"}] 9 | readme = "README.md" 10 | license = { file = "LICENSE" } 11 | classifiers = [] 12 | requires-python = ">=3.8.1" 13 | dependencies = [ 14 | "tomli; python_version < '3.11'", 15 | "flake8 == 7.0.0" 16 | ] 17 | 18 | [project.urls] 19 | Home = "https://github.com/csachs/pyproject-flake8" 20 | 21 | [project.scripts] 22 | pflake8 = "pflake8.__main__:main" 23 | 24 | [tool.flit.module] 25 | name = "pflake8" 26 | 27 | [tool.black] 28 | skip-string-normalization = 1 29 | force-exclude = [ 30 | "test/data/dummy.py", 31 | ] 32 | 33 | [tool.isort] 34 | profile = "black" 35 | multi_line_output = 3 36 | 37 | [tool.flake8] 38 | max-line-length = 88 39 | extend-ignore = ["E203"] 40 | max-complexity = 10 41 | exclude = ["venv", "test/data/"] 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 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 BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pyproject-flake8 (`pflake8`) 2 | 3 | A monkey patching wrapper to connect flake8 with `pyproject.toml` configuration. 4 | 5 | ## Update concerning versioning: 6 | 7 | pyproject-flake8 so far came without explicit pinning of a flake8 version. Since a [recent update](https://github.com/csachs/pyproject-flake8/issues/13) broke compatibility – not unexpectedly – the issue arose, how to handle incompatible versions of flake8 and pyproject-flake8. 8 | Since there are good reasons for and against version pinning, this project now tries to follow a mix of both: 9 | Release versions will follow and pin identical flake8 versions, alpha versions (specific to pyproject-flake8) will pin to the similar non-alpha version of flake8, *or later*. 10 | That way, users of pyproject-flake8 can decide, whether they want a fixed version known to work, or a minimum version, getting later versions, at the risk of future breakage. 11 | 12 | Versions 0.0.1x are pyproject-flake8 internal versions and do not correspond to flake8. Furthermore, flake8 3.8.0 was chosen as the first flake8 version to mirror this way. 13 | 14 | **tl;dr:** 15 | 16 | ```bash 17 | # e.g., suggested installation / dependency ... depending on flake8==5.0.4 18 | 19 | # for Python 3.8+ 20 | pip install pyproject-flake8==5.0.4 21 | 22 | # for Python 3.6+ 23 | pip install pyproject-flake8==5.0.4.post1 24 | ``` 25 | 26 | ## Rationale 27 | 28 | [`flake8`](https://flake8.pycqa.org/) is one of the most popular Python linters, `pyproject.toml` has become the [standard](https://www.python.org/dev/peps/pep-0518/) for Python project metadata. 29 | 30 | More and more tools are able to utilize a shared `pyproject.toml`, alleviating the need for many individual configuration files cluttering a project repository. 31 | 32 | Since excellent `flake8` is not aimed to support `pyproject.toml`, this wrapper script tries to fix the situation. 33 | 34 | ## Installation 35 | 36 | ### From github 37 | 38 | ```bash 39 | pip install . 40 | ``` 41 | 42 | ### From PyPI 43 | 44 | ```bash 45 | pip install pyproject-flake8 46 | ``` 47 | 48 | ### Building packages 49 | 50 | Use your favorite [PEP517](https://www.python.org/dev/peps/pep-0517/) compliant builder, e.g.: 51 | ```bash 52 | # install first via: pip install build 53 | python -m build 54 | # packages will reside in dist/ 55 | ``` 56 | 57 | ## Usage 58 | 59 | Call `pflake8` instead of `flake8`. 60 | 61 | Configuration goes into the `tool.flake8` section of `pyproject.toml`: 62 | 63 | ```toml 64 | [tool.flake8] 65 | max-line-length = 88 66 | extend-ignore = ["E203"] 67 | max-complexity = 10 68 | ``` 69 | 70 | ## See also 71 | 72 | Two other projects aim to address the same problem: 73 | 74 | - [flake9](https://gitlab.com/retnikt/flake9) 75 | - [FlakeHell](https://github.com/life4/flakehell) 76 | 77 | Both seem to try to do a lot more than just getting `pyproject.toml` support. `pyproject-flake8` tries to stay minimal while solving its task. 78 | 79 | [`flake8-pyproject`](https://github.com/john-hen/Flake8-pyproject) adds only `pyproject.toml` support, and does this as a Flake8 plugin, allowing 80 | the original `flake8` command to work (rather than using `pflake8`). 81 | 82 | ## Caveat 83 | 84 | This script monkey-patches flake8 and the configparser library of Python, therefore loading it as a module may have unforeseen consequences. 85 | Alpha quality. Use at your own risk. It will likely break if either Python or flake8 restructure their code significantly. No guarantees for stability between versions. 86 | 87 | ## License 88 | 89 | Unlicense 90 | -------------------------------------------------------------------------------- /pflake8/__init__.py: -------------------------------------------------------------------------------- 1 | """ pyproject-flake8 (`pflake8`), a monkey patching wrapper to connect flake8 with pyproject.toml configuration """ # noqa 2 | 3 | __version__ = '7.0.0' 4 | 5 | import ast 6 | import configparser 7 | import multiprocessing.pool 8 | import sys 9 | from pathlib import Path 10 | from types import ModuleType 11 | 12 | import flake8.main.cli 13 | import flake8.options.config 14 | 15 | try: 16 | # from Python 3.11 onward 17 | from tomllib import load as toml_load 18 | except ImportError: 19 | from tomli import load as toml_load 20 | 21 | 22 | class ConfigParserTomlMixin: 23 | def _read(self, fp, filename): 24 | filename_path = Path(filename) 25 | if filename_path.suffix == '.toml': 26 | is_pyproject = filename_path.name == 'pyproject.toml' 27 | 28 | toml_config = toml_load(fp.buffer) 29 | 30 | section_to_copy = ( 31 | toml_config if not is_pyproject else toml_config.get('tool', {}) 32 | ) 33 | 34 | for section, config in section_to_copy.items(): 35 | try: 36 | self.add_section(section) 37 | except (configparser.DuplicateSectionError): 38 | pass 39 | self._sections.setdefault(section, self._dict()).update(self._dict(config)) 40 | else: 41 | super(ConfigParserTomlMixin, self)._read(fp, filename) 42 | 43 | def _convert_to_boolean(self, value): 44 | if isinstance(value, bool): 45 | return value 46 | else: 47 | return super()._convert_to_boolean(value) 48 | 49 | 50 | class DivertingRawConfigParser(ConfigParserTomlMixin, configparser.RawConfigParser): 51 | pass 52 | 53 | 54 | class DivertingConfigParser(ConfigParserTomlMixin, configparser.ConfigParser): 55 | pass 56 | 57 | 58 | try: 59 | 60 | class DivertingSafeConfigParser( 61 | ConfigParserTomlMixin, configparser.SafeConfigParser 62 | ): 63 | pass 64 | 65 | configparser.SafeConfigParser = DivertingSafeConfigParser 66 | except AttributeError: 67 | pass # does not exist on Python 3.12 (https://github.com/python/cpython/issues/89336#issuecomment-1094366625) 68 | 69 | 70 | configparser.RawConfigParser = DivertingRawConfigParser 71 | configparser.ConfigParser = DivertingConfigParser 72 | 73 | 74 | class FixFilenames(ast.NodeTransformer): 75 | tuple_of_interest = ("setup.cfg", "tox.ini", ".flake8") 76 | 77 | modern = sys.version_info[0] >= 3 and sys.version_info[1] > 7 78 | 79 | inner_type = ast.Constant if modern else ast.Str 80 | inner_type_field = "value" if modern else "s" 81 | 82 | fix_applied = False 83 | 84 | def visit_Tuple(self, node: ast.Tuple) -> ast.Tuple: 85 | if all(isinstance(el, self.inner_type) for el in node.elts) and set( 86 | self.tuple_of_interest 87 | ) == {getattr(el, self.inner_type_field) for el in node.elts}: 88 | node.elts.append( 89 | self.inner_type(**{self.inner_type_field: "pyproject.toml"}) 90 | ) 91 | ast.fix_missing_locations(node) 92 | self.fix_applied = True 93 | return node 94 | 95 | @classmethod 96 | def apply(cls, module: ModuleType = flake8.options.config) -> None: 97 | filename = module.__file__ 98 | 99 | original_ast = ast.parse( 100 | Path(filename).read_text(encoding='UTF-8'), filename=filename 101 | ) 102 | transformer = cls() 103 | fixed_ast = transformer.visit(original_ast) 104 | 105 | if not transformer.fix_applied: 106 | print( 107 | "[pflake8] Warning: Failed applying patch for pyproject.toml parsing.", 108 | file=sys.stderr, 109 | ) 110 | 111 | compiled = compile(fixed_ast, filename=filename, mode="exec") 112 | exec(compiled, module.__dict__) 113 | 114 | 115 | FixFilenames.apply() 116 | 117 | 118 | def _pool_init(real, *args, **kwargs): 119 | # by having the multiprocessing Pool load this function 120 | # the monkeypatches are loaded as well 121 | return real(*args, **kwargs) 122 | 123 | 124 | class PatchingPool(multiprocessing.pool.Pool): 125 | def __init__( 126 | self, 127 | processes=None, 128 | initializer=None, 129 | initargs=(), 130 | maxtasksperchild=None, 131 | context=None, 132 | **kwargs 133 | ): 134 | super().__init__( 135 | processes=processes, 136 | initializer=_pool_init, 137 | initargs=(initializer,) + initargs, 138 | maxtasksperchild=maxtasksperchild, 139 | context=context, 140 | **kwargs 141 | ) 142 | 143 | 144 | if multiprocessing.get_start_method() == "spawn": 145 | multiprocessing.pool.Pool = PatchingPool 146 | 147 | main = flake8.main.cli.main 148 | 149 | __all__ = 'main' 150 | -------------------------------------------------------------------------------- /.github/workflows/maintenance.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from ast import literal_eval 3 | from pathlib import Path 4 | from typing import Any, Dict, List, Tuple, Union 5 | 6 | import requests 7 | import tomli 8 | 9 | 10 | def get_package_info(name: str) -> Dict[str, Any]: 11 | return requests.get(f"https://pypi.org/pypi/{name}/json").json() 12 | 13 | 14 | VERSION = "__version__" 15 | 16 | 17 | def _get_lines(path: Union[Path, str], encoding: str = "utf-8"): 18 | return Path(path).read_text(encoding).splitlines(keepends=True) 19 | 20 | 21 | def get_version( 22 | file_name: Union[Path, str], token: str = VERSION, encoding: str = "utf-8" 23 | ) -> str: 24 | for line in _get_lines(file_name, encoding=encoding): 25 | if line.startswith(token): 26 | return literal_eval(line.split("=", 1)[1]) 27 | raise RuntimeError(f"{token} not found") 28 | 29 | 30 | def patch_version( 31 | file_name: Union[Path, str], 32 | new_version: str, 33 | token: str = VERSION, 34 | encoding: str = "utf-8", 35 | ) -> None: 36 | 37 | patched_string = "".join( 38 | line 39 | if not line.startswith(token) 40 | else f"{token} = '{new_version}'{line[len(line.strip()):]}" 41 | for line in _get_lines(file_name, encoding=encoding) 42 | ) 43 | 44 | Path(file_name).write_text(patched_string, encoding=encoding) 45 | 46 | 47 | def find_dependency_version_string( 48 | pyproject: Union[Path, str], search_string: str 49 | ) -> str: 50 | with Path(pyproject).open('rb') as fp: 51 | result = tomli.load(fp) 52 | 53 | deps = [dep for dep in result["project"]["dependencies"] if search_string in dep] 54 | assert len(deps) == 1, "Multiple dependencies matched!" 55 | return deps[0] 56 | 57 | 58 | def patch_pyproject_string( 59 | pyproject: Union[Path, str], 60 | search_string: str, 61 | replacement_str: str, 62 | quotes: Tuple[str, ...] = ('"', "'"), 63 | encoding: str = "utf-8", 64 | ) -> None: 65 | current = Path(pyproject).read_bytes() 66 | pairs = [ 67 | ( 68 | f"{quote}{search_string}{quote}".encode(encoding), 69 | f"{quote}{replacement_str}{quote}".encode(encoding), 70 | ) 71 | for quote in quotes 72 | ] 73 | matches = [(search, replace) for search, replace in pairs if search in current] 74 | 75 | assert ( 76 | len(matches) == 1 77 | ), f"Either {search_string} is ambiguous or non-existent in {pyproject}." 78 | 79 | search, replace = matches[0] 80 | 81 | split = current.split(search) 82 | assert ( 83 | len(split) == 2 84 | ), f"Trying to replace ambiguous {search_string} in {pyproject}." 85 | 86 | patched = split[0] + replace + split[1] 87 | 88 | Path(pyproject).write_bytes(patched) 89 | 90 | 91 | FLAKE8 = 'flake8' 92 | FLAKE8_MINIMUM_VERSION = '3.8.0' 93 | 94 | PFLAKE8 = 'pyproject-flake8' 95 | PFLAKE8_PYPROJECT = "pyproject.toml" 96 | PFLAKE8_INIT = 'pflake8/__init__.py' 97 | PFLAKE8_PYPROJECT = 'pyproject.toml' 98 | 99 | 100 | def _is_release(version: str) -> bool: 101 | if 'a' in version or 'rc' in version or 'b' in version: 102 | return False 103 | return True 104 | 105 | 106 | def get_flake8_versions() -> List[str]: 107 | flake8_releases = get_package_info(FLAKE8)['releases'] 108 | flake8_versions = list(sorted(flake8_releases.keys(), reverse=True)) 109 | # only consider versions newer or equal than minimum version 110 | flake8_versions = [ 111 | version for version in flake8_versions if version >= FLAKE8_MINIMUM_VERSION 112 | ] 113 | # only consider releases 114 | flake8_versions = [version for version in flake8_versions if _is_release(version)] 115 | 116 | return flake8_versions 117 | 118 | 119 | def get_pflake8_versions() -> List[str]: 120 | pflake8_releases = get_package_info(PFLAKE8)['releases'] 121 | pflake8_versions = list(sorted(pflake8_releases.keys(), reverse=True)) 122 | return pflake8_versions 123 | 124 | 125 | def command_check(): 126 | flake8_versions, pflake8_versions = get_flake8_versions(), get_pflake8_versions() 127 | print(f"Available flake8 versions: {flake8_versions}") 128 | print(f"Available pflake8 versions: {pflake8_versions}") 129 | 130 | missing_versions = list( 131 | sorted(set(flake8_versions) - set(pflake8_versions), reverse=True) 132 | ) 133 | 134 | print(f"The following versions do not have a correspondence: {missing_versions}") 135 | 136 | if missing_versions: 137 | print("There are missing versions!") 138 | return 1 139 | else: 140 | print("All well.") 141 | return 0 142 | 143 | 144 | def str2bool(value: Union[bool, str]) -> bool: 145 | if isinstance(value, bool): 146 | return value 147 | if value.lower() in ('y', 't', 'yes', 'true'): 148 | return True 149 | return False 150 | 151 | 152 | def command_bump(target_version, alpha=False, post=None): 153 | alpha = str2bool(alpha) 154 | suffix = "" if post is None else f".post{int(post)}" 155 | target_version_pflake8 = ( 156 | target_version if not alpha else f"{target_version}a1" 157 | ) + suffix 158 | 159 | flake8_versions, pflake8_versions = get_flake8_versions(), get_pflake8_versions() 160 | 161 | if target_version not in flake8_versions: 162 | raise RuntimeError("Specified version does not exist for flake8!") 163 | 164 | if target_version_pflake8 in pflake8_versions: 165 | raise RuntimeError("Specified version already exists for pflake8!") 166 | 167 | local_pflake8_version = get_version(PFLAKE8_INIT) 168 | 169 | if local_pflake8_version == target_version_pflake8: 170 | raise RuntimeError("Already at specified version.") 171 | 172 | print(f"Current pflake8 version: {local_pflake8_version}") 173 | 174 | print( 175 | f"Bumping to {target_version_pflake8}. " 176 | f"This will be a {'alpha' if alpha else 'normal'} release." 177 | ) 178 | 179 | patch_version(PFLAKE8_INIT, target_version_pflake8) 180 | replacement_string = find_dependency_version_string(PFLAKE8_PYPROJECT, FLAKE8) 181 | 182 | new_version_specification = ( 183 | f"{FLAKE8} >= {target_version}" if alpha else f"{FLAKE8} == {target_version}" 184 | ) 185 | patch_pyproject_string( 186 | PFLAKE8_PYPROJECT, replacement_string, new_version_specification 187 | ) 188 | 189 | todo = [ 190 | f'git add {PFLAKE8_INIT} {PFLAKE8_PYPROJECT}', 191 | f'git commit -m "Version {target_version_pflake8}"', 192 | f'git tag v{target_version_pflake8}', 193 | ] 194 | 195 | print("Done. You should now run:") 196 | 197 | for cmd in todo: 198 | print(cmd) 199 | 200 | 201 | commands = dict( 202 | check=command_check, 203 | bump=command_bump, 204 | ) 205 | 206 | 207 | def main(args: List[str] = sys.argv[1:]): 208 | if not args: 209 | print(f"Supported commands are {tuple(commands.keys())!r}") 210 | return 1 211 | 212 | return commands[args[0]](*args[1:]) 213 | 214 | 215 | if __name__ == '__main__': 216 | sys.exit(main()) 217 | --------------------------------------------------------------------------------