├── .gitignore ├── generate-readme ├── LICENSE ├── .pre-commit-config.yaml ├── README.md ├── .pre-commit-hooks.yaml └── tests └── hooks_test.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | -------------------------------------------------------------------------------- /generate-readme: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from __future__ import annotations 3 | 4 | import yaml 5 | 6 | Loader = getattr(yaml, 'CSafeLoader', yaml.SafeLoader) 7 | 8 | 9 | def main() -> int: 10 | with open('.pre-commit-hooks.yaml') as f: 11 | hooks = yaml.load(f, Loader=Loader) 12 | 13 | with open('README.md') as f: 14 | contents = f.read() 15 | before, delim, _ = contents.partition('[generated]: # (generated)\n') 16 | 17 | rest = '\n'.join( 18 | f'- **`{hook["id"]}`**: {hook["description"]}' for hook in hooks 19 | ) 20 | 21 | with open('README.md', 'w') as f: 22 | f.write(before + delim + rest + '\n') 23 | 24 | return 0 25 | 26 | 27 | if __name__ == '__main__': 28 | raise SystemExit(main()) 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 Anthony Sottile 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 11 | all 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 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v6.0.0 4 | hooks: 5 | - id: trailing-whitespace 6 | - id: end-of-file-fixer 7 | - id: check-yaml 8 | - id: debug-statements 9 | - id: double-quote-string-fixer 10 | - id: name-tests-test 11 | - id: requirements-txt-fixer 12 | - repo: https://github.com/asottile/reorder-python-imports 13 | rev: v3.16.0 14 | hooks: 15 | - id: reorder-python-imports 16 | args: [--py310-plus, --add-import, 'from __future__ import annotations'] 17 | - repo: https://github.com/asottile/add-trailing-comma 18 | rev: v4.0.0 19 | hooks: 20 | - id: add-trailing-comma 21 | - repo: https://github.com/asottile/pyupgrade 22 | rev: v3.21.2 23 | hooks: 24 | - id: pyupgrade 25 | args: [--py310-plus] 26 | - repo: https://github.com/hhatto/autopep8 27 | rev: v2.3.2 28 | hooks: 29 | - id: autopep8 30 | - repo: https://github.com/PyCQA/flake8 31 | rev: 7.3.0 32 | hooks: 33 | - id: flake8 34 | - repo: https://github.com/pre-commit/mirrors-mypy 35 | rev: v1.19.1 36 | hooks: 37 | - id: mypy 38 | additional_dependencies: [types-pyyaml] 39 | - repo: local 40 | hooks: 41 | - id: generate-readme 42 | name: generate readme 43 | entry: ./generate-readme 44 | language: python 45 | additional_dependencies: [pyyaml] 46 | files: ^(\.pre-commit-hooks.yaml|generate-readme)$ 47 | pass_filenames: false 48 | - id: run-tests 49 | name: run tests 50 | entry: pytest tests 51 | language: python 52 | additional_dependencies: [pre-commit, pytest] 53 | always_run: true 54 | pass_filenames: false 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![pre-commit.ci status](https://results.pre-commit.ci/badge/github/pre-commit/pygrep-hooks/main.svg)](https://results.pre-commit.ci/latest/github/pre-commit/pygrep-hooks/main) 2 | 3 | pygrep-hooks 4 | ============ 5 | 6 | A collection of fast, cheap, regex based pre-commit hooks. 7 | 8 | 9 | ### Adding to your `.pre-commit-config.yaml` 10 | 11 | ```yaml 12 | - repo: https://github.com/pre-commit/pygrep-hooks 13 | rev: v1.10.0 # Use the ref you want to point at 14 | hooks: 15 | - id: python-use-type-annotations 16 | # ... 17 | ``` 18 | 19 | ### Naming conventions 20 | 21 | Where possible, these hooks will be prefixed with the file types they target. 22 | For example, a hook which targets python will be called `python-...`. 23 | 24 | ### Provided hooks 25 | 26 | [generated]: # (generated) 27 | - **`python-check-blanket-noqa`**: Enforce that `noqa` annotations always occur with specific codes. Sample annotations: `# noqa: F401`, `# noqa: F401,W203` 28 | - **`python-check-blanket-type-ignore`**: Enforce that `# type: ignore` annotations always occur with specific codes. Sample annotations: `# type: ignore[attr-defined]`, `# type: ignore[attr-defined, name-defined]` 29 | - **`python-check-mock-methods`**: Prevent common mistakes of `assert mck.not_called()`, `assert mck.called_once_with(...)` and `mck.assert_called`. 30 | - **`python-no-eval`**: A quick check for the `eval()` built-in function 31 | - **`python-no-log-warn`**: A quick check for the deprecated `.warn()` method of python loggers 32 | - **`python-use-type-annotations`**: Enforce that python3.6+ type annotations are used instead of type comments 33 | - **`rst-backticks`**: Detect common mistake of using single backticks when writing rst 34 | - **`rst-directive-colons`**: Detect mistake of rst directive not ending with double colon or space before the double colon 35 | - **`rst-inline-touching-normal`**: Detect mistake of inline code touching normal text in rst 36 | - **`text-unicode-replacement-char`**: Forbid files which have a UTF-8 Unicode replacement character 37 | -------------------------------------------------------------------------------- /.pre-commit-hooks.yaml: -------------------------------------------------------------------------------- 1 | - id: python-check-blanket-noqa 2 | name: check blanket noqa 3 | description: 'Enforce that `noqa` annotations always occur with specific codes. Sample annotations: `# noqa: F401`, `# noqa: F401,W203`' 4 | entry: '(?i)# noqa(?!: )' 5 | language: pygrep 6 | types: [python] 7 | - id: python-check-blanket-type-ignore 8 | name: check blanket type ignore 9 | description: 'Enforce that `# type: ignore` annotations always occur with specific codes. Sample annotations: `# type: ignore[attr-defined]`, `# type: ignore[attr-defined, name-defined]`' 10 | entry: '# type:? *ignore(?!\[|\w)' 11 | language: pygrep 12 | types: [python] 13 | - id: python-check-mock-methods 14 | name: check for not-real mock methods 15 | description: >- 16 | Prevent common mistakes of `assert mck.not_called()`, `assert mck.called_once_with(...)` 17 | and `mck.assert_called`. 18 | language: pygrep 19 | entry: > 20 | (?x)( 21 | assert .*\.( 22 | not_called| 23 | called_ 24 | )| 25 | # ''.join(rf'(? quoted `literal` block', 168 | ), 169 | ) 170 | def test_python_rst_backticks_positive(s): 171 | assert HOOKS['rst-backticks'].search(s) 172 | 173 | 174 | @pytest.mark.parametrize( 175 | 's', 176 | ( 177 | ' ``[code]``', 178 | 'i like _`kitty`', 179 | 'i like `kitty`_', 180 | '``b``', 181 | '``ef``', 182 | ' indented `literal` block', 183 | ), 184 | ) 185 | def test_python_rst_backticks_negative(s): 186 | assert not HOOKS['rst-backticks'].search(s) 187 | 188 | 189 | @pytest.mark.parametrize( 190 | 's', 191 | ( 192 | '``PyMem_Realloc()`` indirectly call``PyObject_Malloc()`` and', 193 | 'This PEP proposes that ``bytes`` and ``bytearray``gain an optimised', 194 | 'Reading this we first see the``break``, which obviously applies to', 195 | 'for using``long_description`` and a corresponding', 196 | '``inline`` normal``inline', 197 | '``inline``normal ``inline', 198 | '``inline``normal', 199 | '``inline``normal``inline', 200 | 'normal ``inline``normal', 201 | 'normal``inline`` normal', 202 | 'normal``inline``', 203 | 'normal``inline``normal', 204 | ), 205 | ) 206 | def test_python_rst_inline_touching_normal_positive(s): 207 | assert HOOKS['rst-inline-touching-normal'].search(s) 208 | 209 | 210 | @pytest.mark.parametrize( 211 | 's', 212 | ( 213 | '``PyMem_Realloc()`` indirectly call ``PyObject_Malloc()`` and', 214 | 'This PEP proposes that ``bytes`` and ``bytearray`` gain an optimised', 215 | 'Reading this we first see the ``break``, which obviously applies to', 216 | 'for using ``long_description`` and a corresponding', 217 | '``inline`` normal ``inline', 218 | '``inline`` normal', 219 | 'normal ``inline`` normal', 220 | 'normal ``inline``', 221 | ), 222 | ) 223 | def test_python_rst_inline_touching_normal_negative(s): 224 | assert not HOOKS['rst-inline-touching-normal'].search(s) 225 | 226 | 227 | @pytest.mark.parametrize( 228 | 's', 229 | ( 230 | str(b'\x80abc', errors='replace'), 231 | ), 232 | ) 233 | def test_text_unicode_replacement_char_positive(s): 234 | assert HOOKS['text-unicode-replacement-char'].search(s) 235 | 236 | 237 | @pytest.mark.parametrize( 238 | 's', 239 | ( 240 | 'foo', 241 | ), 242 | ) 243 | def test_text_unicode_replacement_char_negative(s): 244 | assert not HOOKS['text-unicode-replacement-char'].search(s) 245 | 246 | 247 | @pytest.mark.parametrize( 248 | 's', 249 | ( 250 | ' .. warning:', 251 | '.. warning:', 252 | ' .. warning ::', 253 | '.. warning ::', 254 | ' .. warning :', 255 | '.. warning :', 256 | ), 257 | ) 258 | def test_rst_directive_colons_positive(s): 259 | assert HOOKS['rst-directive-colons'].search(s) 260 | 261 | 262 | @pytest.mark.parametrize( 263 | 's', 264 | ( 265 | '.. warning::', 266 | '.. code:: python', 267 | ), 268 | ) 269 | def test_rst_directive_colons_negative(s): 270 | assert not HOOKS['rst-directive-colons'].search(s) 271 | 272 | 273 | def test_that_hooks_are_sorted(): 274 | assert list(HOOKS) == sorted(HOOKS) 275 | --------------------------------------------------------------------------------