Compare commits

..

No commits in common. "main" and "v2.5.0" have entirely different histories.
main ... v2.5.0

87 changed files with 1300 additions and 2286 deletions

29
.coveragerc Normal file
View file

@ -0,0 +1,29 @@
[run]
branch = True
source =
.
omit =
.tox/*
/usr/*
setup.py
[report]
show_missing = True
skip_covered = True
exclude_lines =
# Have to re-enable the standard pragma
\#\s*pragma: no cover
# Don't complain if tests don't hit defensive assertion code:
^\s*raise AssertionError\b
^\s*raise NotImplementedError\b
^\s*return NotImplemented\b
^\s*raise$
# Don't complain if non-runnable code isn't run:
^if __name__ == ['"]__main__['"]:$
[html]
directory = coverage-html
# vim:ft=dosini

View file

@ -1,19 +0,0 @@
name: main
on:
push:
branches: [main, test-me-*]
tags: '*'
pull_request:
jobs:
main-windows:
uses: asottile/workflows/.github/workflows/tox.yml@v1.8.1
with:
env: '["py310"]'
os: windows-latest
main-linux:
uses: asottile/workflows/.github/workflows/tox.yml@v1.8.1
with:
env: '["py310", "py311", "py312", "py313"]'
os: ubuntu-latest

5
.gitignore vendored
View file

@ -3,4 +3,9 @@
.*.sw[a-z] .*.sw[a-z]
.coverage .coverage
.tox .tox
.venv.touch
/.mypy_cache
/.pytest_cache
/venv*
coverage-html
dist dist

View file

@ -1,41 +1,44 @@
repos: repos:
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0 rev: v2.5.0
hooks: hooks:
- id: trailing-whitespace - id: trailing-whitespace
- id: end-of-file-fixer - id: end-of-file-fixer
- id: check-docstring-first
- id: check-json
- id: check-added-large-files
- id: check-yaml - id: check-yaml
- id: debug-statements - id: debug-statements
- id: double-quote-string-fixer
- id: name-tests-test - id: name-tests-test
- id: double-quote-string-fixer
- id: requirements-txt-fixer - id: requirements-txt-fixer
- repo: https://github.com/asottile/setup-cfg-fmt - repo: https://gitlab.com/pycqa/flake8
rev: v3.2.0 rev: 3.7.9
hooks:
- id: setup-cfg-fmt
- repo: https://github.com/asottile/reorder-python-imports
rev: v3.16.0
hooks:
- id: reorder-python-imports
args: [--py310-plus, --add-import, 'from __future__ import annotations']
- repo: https://github.com/asottile/add-trailing-comma
rev: v4.0.0
hooks:
- id: add-trailing-comma
- repo: https://github.com/asottile/pyupgrade
rev: v3.21.2
hooks:
- id: pyupgrade
args: [--py310-plus]
- repo: https://github.com/hhatto/autopep8
rev: v2.3.2
hooks:
- id: autopep8
- repo: https://github.com/PyCQA/flake8
rev: 7.3.0
hooks: hooks:
- id: flake8 - id: flake8
- repo: https://github.com/pre-commit/mirrors-autopep8
rev: v1.5
hooks:
- id: autopep8
- repo: https://github.com/pre-commit/pre-commit
rev: v2.0.1
hooks:
- id: validate_manifest
- repo: https://github.com/asottile/reorder_python_imports
rev: v1.9.0
hooks:
- id: reorder-python-imports
language_version: python3
- repo: https://github.com/asottile/pyupgrade
rev: v1.26.2
hooks:
- id: pyupgrade
- repo: https://github.com/asottile/add-trailing-comma
rev: v1.5.0
hooks:
- id: add-trailing-comma
- repo: https://github.com/pre-commit/mirrors-mypy - repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.19.1 rev: v0.761
hooks: hooks:
- id: mypy - id: mypy
language_version: python3

View file

@ -1,212 +1,194 @@
- id: autopep8-wrapper
name: autopep8 wrapper
description: This is deprecated, use pre-commit/mirrors-autopep8 instead.
entry: autopep8-wrapper
language: python
types: [python]
args: [-i]
- id: check-added-large-files - id: check-added-large-files
name: check for added large files name: Check for added large files
description: prevents giant files from being committed. description: Prevent giant files from being committed
entry: check-added-large-files entry: check-added-large-files
language: python language: python
stages: [pre-commit, pre-push, manual]
minimum_pre_commit_version: 3.2.0
- id: check-ast - id: check-ast
name: check python ast name: Check python ast
description: simply checks whether the files parse as valid python. description: Simply check whether the files parse as valid python.
entry: check-ast entry: check-ast
language: python language: python
types: [python] types: [python]
- id: check-byte-order-marker - id: check-byte-order-marker
name: check-byte-order-marker (removed) name: Check for byte-order marker
description: (removed) use fix-byte-order-marker instead. description: Forbid files which have a UTF-8 byte-order marker
entry: pre-commit-hooks-removed check-byte-order-marker fix-byte-order-marker https://github.com/pre-commit/pre-commit-hooks entry: check-byte-order-marker
language: python language: python
types: [text] types: [text]
- id: check-builtin-literals - id: check-builtin-literals
name: check builtin type constructor use name: Check builtin type constructor use
description: requires literal syntax when initializing empty or zero python builtin types. description: Require literal syntax when initializing empty or zero Python builtin types.
entry: check-builtin-literals entry: check-builtin-literals
language: python language: python
types: [python] types: [python]
- id: check-case-conflict - id: check-case-conflict
name: check for case conflicts name: Check for case conflicts
description: checks for files that would conflict in case-insensitive filesystems. description: Check for files that would conflict in case-insensitive filesystems
entry: check-case-conflict entry: check-case-conflict
language: python language: python
- id: check-docstring-first - id: check-docstring-first
name: check docstring is first (deprecated) name: Check docstring is first
description: checks a common error of defining a docstring after code. description: Checks a common error of defining a docstring after code.
entry: check-docstring-first entry: check-docstring-first
language: python language: python
types: [python] types: [python]
- id: check-executables-have-shebangs - id: check-executables-have-shebangs
name: check that executables have shebangs name: Check that executables have shebangs
description: ensures that (non-binary) executables have a shebang. description: Ensures that (non-binary) executables have a shebang.
entry: check-executables-have-shebangs entry: check-executables-have-shebangs
language: python language: python
types: [text, executable] types: [text, executable]
stages: [pre-commit, pre-push, manual] stages: [commit, push, manual]
minimum_pre_commit_version: 3.2.0
- id: check-illegal-windows-names
name: check illegal windows names
entry: Illegal Windows filenames detected
language: fail
files: '(?i)((^|/)(CON|PRN|AUX|NUL|COM[\d¹²³]|LPT[\d¹²³])(\.|/|$)|[<>:\"\\|?*\x00-\x1F]|/[^/]*[\.\s]/|[^/]*[\.\s]$)'
- id: check-json - id: check-json
name: check json name: Check JSON
description: checks json files for parseable syntax. description: This hook checks json files for parseable syntax.
entry: check-json entry: check-json
language: python language: python
types: [json] types: [json]
- id: check-shebang-scripts-are-executable
name: check that scripts with shebangs are executable
description: ensures that (non-binary) files with a shebang are executable.
entry: check-shebang-scripts-are-executable
language: python
types: [text]
stages: [pre-commit, pre-push, manual]
minimum_pre_commit_version: 3.2.0
- id: pretty-format-json - id: pretty-format-json
name: pretty format json name: Pretty format JSON
description: sets a standard for formatting json files. description: This hook sets a standard for formatting JSON files.
entry: pretty-format-json entry: pretty-format-json
language: python language: python
types: [json] types: [json]
- id: check-merge-conflict - id: check-merge-conflict
name: check for merge conflicts name: Check for merge conflicts
description: checks for files that contain merge conflict strings. description: Check for files that contain merge conflict strings.
entry: check-merge-conflict entry: check-merge-conflict
language: python language: python
types: [text] types: [text]
- id: check-symlinks - id: check-symlinks
name: check for broken symlinks name: Check for broken symlinks
description: checks for symlinks which do not point to anything. description: Checks for symlinks which do not point to anything.
entry: check-symlinks entry: check-symlinks
language: python language: python
types: [symlink] types: [symlink]
- id: check-toml - id: check-toml
name: check toml name: Check Toml
description: checks toml files for parseable syntax. description: This hook checks toml files for parseable syntax.
entry: check-toml entry: check-toml
language: python language: python
types: [toml] types: [toml]
- id: check-vcs-permalinks - id: check-vcs-permalinks
name: check vcs permalinks name: Check vcs permalinks
description: ensures that links to vcs websites are permalinks. description: Ensures that links to vcs websites are permalinks.
entry: check-vcs-permalinks entry: check-vcs-permalinks
language: python language: python
types: [text] types: [text]
- id: check-xml - id: check-xml
name: check xml name: Check Xml
description: checks xml files for parseable syntax. description: This hook checks xml files for parseable syntax.
entry: check-xml entry: check-xml
language: python language: python
types: [xml] types: [xml]
- id: check-yaml - id: check-yaml
name: check yaml name: Check Yaml
description: checks yaml files for parseable syntax. description: This hook checks yaml files for parseable syntax.
entry: check-yaml entry: check-yaml
language: python language: python
types: [yaml] types: [yaml]
- id: debug-statements - id: debug-statements
name: debug statements (python) name: Debug Statements (Python)
description: checks for debugger imports and py37+ `breakpoint()` calls in python source. description: Check for debugger imports and py37+ `breakpoint()` calls in python source.
entry: debug-statement-hook entry: debug-statement-hook
language: python language: python
types: [python] types: [python]
- id: destroyed-symlinks
name: detect destroyed symlinks
description: detects symlinks which are changed to regular files with a content of a path which that symlink was pointing to.
entry: destroyed-symlinks
language: python
types: [file]
stages: [pre-commit, pre-push, manual]
- id: detect-aws-credentials - id: detect-aws-credentials
name: detect aws credentials name: Detect AWS Credentials
description: detects *your* aws credentials from the aws cli credentials file. description: Detects *your* aws credentials from the aws cli credentials file
entry: detect-aws-credentials entry: detect-aws-credentials
language: python language: python
types: [text] types: [text]
- id: detect-private-key - id: detect-private-key
name: detect private key name: Detect Private Key
description: detects the presence of private keys. description: Detects the presence of private keys
entry: detect-private-key entry: detect-private-key
language: python language: python
types: [text] types: [text]
- id: double-quote-string-fixer - id: double-quote-string-fixer
name: fix double quoted strings name: Fix double quoted strings
description: replaces double quoted strings with single quoted strings. description: This hook replaces double quoted strings with single quoted strings
entry: double-quote-string-fixer entry: double-quote-string-fixer
language: python language: python
types: [python] types: [python]
- id: end-of-file-fixer - id: end-of-file-fixer
name: fix end of files name: Fix End of Files
description: ensures that a file is either empty, or ends with one newline. description: Ensures that a file is either empty, or ends with one newline.
entry: end-of-file-fixer entry: end-of-file-fixer
language: python language: python
types: [text] types: [text]
stages: [pre-commit, pre-push, manual] stages: [commit, push, manual]
minimum_pre_commit_version: 3.2.0
- id: file-contents-sorter - id: file-contents-sorter
name: file contents sorter name: File Contents Sorter
description: sorts the lines in specified files (defaults to alphabetical). you must provide list of target files as input in your .pre-commit-config.yaml file. description: Sort the lines in specified files (defaults to alphabetical). You must provide list of target files as input in your .pre-commit-config.yaml file.
entry: file-contents-sorter entry: file-contents-sorter
language: python language: python
files: '^$' files: '^$'
- id: fix-byte-order-marker
name: fix utf-8 byte order marker
description: removes utf-8 byte order marker.
entry: fix-byte-order-marker
language: python
types: [text]
- id: fix-encoding-pragma - id: fix-encoding-pragma
name: fix python encoding pragma (removed) name: Fix python encoding pragma
description: (removed) use pyupgrade instead. language: python
entry: pre-commit-hooks-removed fix-encoding-pragma pyupgrade https://github.com/asottile/pyupgrade entry: fix-encoding-pragma
description: 'Add # -*- coding: utf-8 -*- to the top of python files'
types: [python]
- id: flake8
name: Flake8 (deprecated, use gitlab.com/pycqa/flake8)
description: This hook runs flake8.
entry: flake8
language: python language: python
types: [python] types: [python]
require_serial: true
- id: forbid-new-submodules - id: forbid-new-submodules
name: forbid new submodules name: Forbid new submodules
description: prevents addition of new git submodules.
language: python language: python
entry: forbid-new-submodules entry: forbid-new-submodules
types: [directory] description: Prevent addition of new git submodules
- id: forbid-submodules
name: forbid submodules
description: forbids any submodules in the repository
language: fail
entry: 'submodules are not allowed in this repository:'
types: [directory]
- id: mixed-line-ending - id: mixed-line-ending
name: mixed line ending name: Mixed line ending
description: replaces or checks mixed line ending. description: Replaces or checks mixed line ending
entry: mixed-line-ending entry: mixed-line-ending
language: python language: python
types: [text] types: [text]
- id: name-tests-test - id: name-tests-test
name: python tests naming name: Tests should end in _test.py
description: verifies that test files are named correctly. description: This verifies that test files are named correctly
entry: name-tests-test entry: name-tests-test
language: python language: python
files: (^|/)tests/.+\.py$ files: (^|/)tests/.+\.py$
- id: no-commit-to-branch - id: no-commit-to-branch
name: "don't commit to branch" name: "Don't commit to branch"
entry: no-commit-to-branch entry: no-commit-to-branch
language: python language: python
pass_filenames: false pass_filenames: false
always_run: true always_run: true
- id: pyflakes
name: Pyflakes (DEPRECATED, use flake8)
description: This hook runs pyflakes. (This is deprecated, use flake8).
entry: pyflakes
language: python
types: [python]
- id: requirements-txt-fixer - id: requirements-txt-fixer
name: fix requirements.txt name: Fix requirements.txt
description: sorts entries in requirements.txt. description: Sorts entries in requirements.txt
entry: requirements-txt-fixer entry: requirements-txt-fixer
language: python language: python
files: (requirements|constraints).*\.txt$ files: requirements.*\.txt$
- id: sort-simple-yaml - id: sort-simple-yaml
name: sort simple yaml files name: Sort simple YAML files
description: sorts simple yaml files which consist only of top-level keys, preserving comments and blocks.
language: python language: python
entry: sort-simple-yaml entry: sort-simple-yaml
description: Sorts simple YAML files which consist only of top-level keys, preserving comments and blocks.
files: '^$' files: '^$'
- id: trailing-whitespace - id: trailing-whitespace
name: trim trailing whitespace name: Trim Trailing Whitespace
description: trims trailing whitespace. description: This hook trims trailing whitespace.
entry: trailing-whitespace-fixer entry: trailing-whitespace-fixer
language: python language: python
types: [text] types: [text]
stages: [pre-commit, pre-push, manual] stages: [commit, push, manual]
minimum_pre_commit_version: 3.2.0

View file

@ -1,320 +1,3 @@
6.0.0 - 2025-08-09
==================
## Fixes
- `check-shebang-scripts-are-executable`: improve error message.
- #1115 PR by @homebysix.
## Migrating
- now requires python >= 3.9.
- #1098 PR by @asottile.
- `file-contents-sorter`: disallow `--unique` and `--ignore-case` at the same
time.
- #1095 PR by @nemacysts.
- #794 issue by @teksturi.
- Removed `check-byte-order-marker` and `fix-encoding-pragma`.
- `check-byte-order-marker`: migrate to `fix-byte-order-marker`.
- `fix-encoding-pragma`: migrate to `pyupgrade`.
- #1034 PR by @mxr.
- #1032 issue by @mxr.
- #522 PR by @jgowdy.
5.0.0 - 2024-10-05
==================
### Features
- `requirements-txt-fixer`: also remove `pkg_resources==...`.
- #850 PR by @ericfrederich.
- #1030 issue by @ericfrederich.
- `check-illegal-windows-names`: new hook!
- #1044 PR by @ericfrederich.
- #589 issue by @ericfrederich.
- #1049 PR by @Jeffrey-Lim.
- `pretty-format-json`: continue processing even if a file has a json error.
- #1039 PR by @amarvin.
- #1038 issue by @amarvin.
### Fixes
- `destroyed-symlinks`: set `stages` to `[pre-commit, pre-push, manual]`
- PR #1085 by @AdrianDC.
### Migrating
- pre-commit-hooks now requires `pre-commit>=3.2.0`.
- use non-deprecated names for `stages`.
- #1093 PR by @asottile.
4.6.0 - 2024-04-06
==================
### Features
- `requirements-txt-fixer`: remove duplicate packages.
- #1014 PR by @vhoulbreque-withings.
- #960 issue @csibe17.
### Migrating
- `fix-encoding-pragma`: deprecated -- will be removed in 5.0.0. use
[pyupgrade](https://github.com/asottile/pyupgrade) or some other tool.
- #1033 PR by @mxr.
- #1032 issue by @mxr.
4.5.0 - 2023-10-07
==================
### Features
- `requirements-txt-fixer`: also sort `constraints.txt` by default.
- #857 PR by @lev-blit.
- #830 issue by @PLPeeters.
- `debug-statements`: add `bpdb` debugger.
- #942 PR by @mwip.
- #941 issue by @mwip.
### Fixes
- `file-contents-sorter`: fix sorting an empty file.
- #944 PR by @RoelAdriaans.
- #935 issue by @paduszyk.
- `double-quote-string-fixer`: don't rewrite inside f-strings in 3.12+.
- #973 PR by @asottile.
- #971 issue by @XuehaiPan.
## Migrating
- now requires python >= 3.8.
- #926 PR by @asottile.
- #927 PR by @asottile.
4.4.0 - 2022-11-23
==================
### Features
- `forbid-submodules`: new hook which outright bans submodules.
- #815 PR by @asottile.
- #707 issue by @ChiefGokhlayeh.
4.3.0 - 2022-06-07
==================
### Features
- `check-executables-have-shebangs`: use `git config core.fileMode` to
determine if it should query `git`.
- #730 PR by @Kurt-von-Laven.
- `name-tests-test`: add `--pytest-test-first` test convention.
- #779 PR by @asottile.
### Fixes
- `check-shebang-scripts-are-executable`: update windows instructions.
- #774 PR by @mdeweerd.
- #770 issue by @mdeweerd.
- `check-toml`: use stdlib `tomllib` when available.
- #771 PR by @DanielNoord.
- #755 issue by @sognetic.
- `check-added-large-files`: don't run on non-file `stages`.
- #778 PR by @asottile.
- #777 issue by @skyj.
4.2.0 - 2022-04-06
==================
### Features
- `name-tests-test`: updated display text.
- #713 PR by @asottile.
- `check-docstring-first`: make output more parsable.
- #748 PR by @asottile.
- `check-merge-conflict`: make output more parsable.
- #748 PR by @asottile.
- `debug-statements`: make output more parsable.
- #748 PR by @asottile.
### Fixes
- `check-merge-conflict`: fix detection of `======` conflict marker on windows.
- #748 PR by @asottile.
### Updating
- Drop python<3.7.
- #719 PR by @asottile.
- Changed default branch from `master` to `main`.
- #744 PR by @asottile.
4.1.0 - 2021-12-22
==================
### Features
- `debug-statements`: add `pdbr` debugger.
- #614 PR by @cansarigol.
- `detect-private-key`: add detection for additional key types.
- #658 PR by @ljmf00.
- `check-executables-have-shebangs`: improve messaging on windows.
- #689 PR by @pujitm.
- #686 issue by @jmerdich.
- `check-added-large-files`: support `--enforce-all` with `git-lfs`.
- #674 PR by @amartani.
- #560 issue by @jeremy-coulon.
### Fixes
- `check-case-conflict`: improve performance.
- #626 PR by @guykisel.
- #625 issue by @guykisel.
- `forbid-new-submodules`: fix false-negatives for `pre-push`.
- #619 PR by @m-khvoinitsky.
- #609 issue by @m-khvoinitsky.
- `check-merge-conflict`: fix execution in git worktrees.
- #662 PR by @errsyn.
- #638 issue by @daschuer.
### Misc.
- Normalize case of hook names and descriptions.
- #671 PR by @dennisroche.
- #673 PR by @revolter.
4.0.1 - 2021-05-16
==================
### Fixes
- `check-shebang-scripts-are-executable` fix entry point.
- #602 issue by @Person-93.
- #603 PR by @scop.
4.0.0 - 2021-05-14
==================
### Features
- `check-json`: report duplicate keys.
- #558 PR by @AdityaKhursale.
- #554 issue by @adamchainz.
- `no-commit-to-branch`: add `main` to default blocked branches.
- #565 PR by @ndevenish.
- `check-case-conflict`: check conflicts in directory names as well.
- #575 PR by @slsyy.
- #70 issue by @andyjack.
- `check-vcs-permalinks`: forbid other branch names.
- #582 PR by @jack1142.
- #581 issue by @jack1142.
- `check-shebang-scripts-are-executable`: new hook which ensures shebang'd
scripts are executable.
- #545 PR by @scop.
### Fixes
- `check-executables-have-shebangs`: Short circuit shebang lookup on windows.
- #544 PR by @scop.
- `requirements-txt-fixer`: Fix comments which have indentation
- #549 PR by @greshilov.
- #548 issue by @greshilov.
- `pretty-format-json`: write to stdout using UTF-8 encoding.
- #571 PR by @jack1142.
- #570 issue by @jack1142.
- Use more inclusive language.
- #599 PR by @asottile.
### Breaking changes
- Remove deprecated hooks: `flake8`, `pyflakes`, `autopep8-wrapper`.
- #597 PR by @asottile.
3.4.0 - 2020-12-15
==================
### Features
- `file-contents-sorter`: Add `--unique` argument
- #524 PR by @danielhoherd.
- `check-vcs-permalinks`: Add `--additional-github-domain` option
- #530 PR by @youngminz.
- New hook: `destroyed-symlinks` to detect unintentional symlink-breakages on
windows.
- #511 PR by @m-khvoinitsky.
3.3.0 - 2020-10-20
==================
### Features
- `file-contents-sorter`: add `--ignore-case` option for case-insensitive
sorting
- #514 PR by @Julian.
- `check-added-large-files`: add `--enforce-all` option to check non-added
files as well
- #519 PR by @mshawcroft.
- #518 issue by @mshawcroft.
- `fix-byte-order-marker`: new hook which fixes UTF-8 byte-order marker.
- #522 PR by @jgowdy.
### Deprecations
- `check-byte-order-marker` is now deprecated for `fix-byte-order-marker`
3.2.0 - 2020-07-30
==================
### Features
- `debug-statements`: add support for `pydevd_pycharm` debugger
- #502 PR by @jgeerds.
### Fixes
- `check-executables-have-shebangs`: fix git-quoted files on windows (spaces,
non-ascii, etc.)
- #509 PR by @pawamoy.
- #508 issue by @pawamoy.
3.1.0 - 2020-05-20
==================
### Features
- `check-executables-have-shebangs`: on windows, validate the mode bits using
`git`
- #480 PR by @mxr.
- #435 issue by @dstandish.
- `requirements-txt-fixer`: support more operators
- #483 PR by @mxr.
- #331 issue by @hackedd.
### Fixes
- `pre-commit-hooks-removed`: Fix when removed hooks used `args`
- #487 PR by @pedrocalleja.
- #485 issue by @pedrocalleja.
3.0.1 - 2020-05-16
==================
### Fixes
- `check-toml`: use UTF-8 encoding to load toml files
- #479 PR by @mxr.
- #474 issue by @staticdev.
3.0.0 - 2020-05-14
==================
### Features
- `detect-aws-credentials`: skip empty aws keys
- #450 PR by @begoon.
- #449 issue by @begoon.
- `debug-statements`: add detection `wdb` debugger
- #452 PR by @itsdkey.
- #451 issue by @itsdkey.
- `requirements-txt-fixer`: support line continuation for dependencies
- #469 PR by @aniketbhatnagar.
- #465 issue by @aniketbhatnagar.
### Fixes
- `detect-aws-credentials`: fix `UnicodeDecodeError` when running on non-UTF8
files.
- #453 PR by @asottile.
- #393 PR by @a7p
- #346 issue by @rpdelaney.
### Updating
- pre-commit/pre-commit-hooks now requires python3.6.1+
- #447 PR by @asottile.
- #455 PR by @asottile.
- `flake8` / `pyflakes` have been removed, use `flake8` from `pycqa/flake8`
instead:
```yaml
- repo: https://gitlab.com/pycqa/flake8
rev: 3.8.1
hooks:
- id: flake8
```
- #476 PR by @asottile.
- #477 PR by @asottile.
- #344 issue by @asottile.
2.5.0 - 2020-02-04 2.5.0 - 2020-02-04
================== ==================
@ -322,7 +5,7 @@
- Fix sorting of requirements which use `egg=...` - Fix sorting of requirements which use `egg=...`
- #425 PR by @vinayinvicible. - #425 PR by @vinayinvicible.
- Fix over-eager regular expression for test filename matching - Fix over-eager regular expression for test filename matching
- #429 PR by @rrauenza. - #429 PR by rrauenza.
### Updating ### Updating
- Use `flake8` from `pycqa/flake8` instead: - Use `flake8` from `pycqa/flake8` instead:

View file

@ -1,5 +1,5 @@
[![build status](https://github.com/pre-commit/pre-commit-hooks/actions/workflows/main.yml/badge.svg)](https://github.com/pre-commit/pre-commit-hooks/actions/workflows/main.yml) [![Build Status](https://asottile.visualstudio.com/asottile/_apis/build/status/pre-commit.pre-commit-hooks?branchName=master)](https://asottile.visualstudio.com/asottile/_build/latest?definitionId=17&branchName=master)
[![pre-commit.ci status](https://results.pre-commit.ci/badge/github/pre-commit/pre-commit-hooks/main.svg)](https://results.pre-commit.ci/latest/github/pre-commit/pre-commit-hooks/main) [![Azure DevOps coverage](https://img.shields.io/azure-devops/coverage/asottile/asottile/17/master.svg)](https://dev.azure.com/asottile/asottile/_build/latest?definitionId=17&branchName=master)
pre-commit-hooks pre-commit-hooks
================ ================
@ -13,24 +13,20 @@ See also: https://github.com/pre-commit/pre-commit
Add this to your `.pre-commit-config.yaml` Add this to your `.pre-commit-config.yaml`
```yaml - repo: https://github.com/pre-commit/pre-commit-hooks
- repo: https://github.com/pre-commit/pre-commit-hooks rev: v2.5.0 # Use the ref you want to point at
rev: v6.0.0 # Use the ref you want to point at
hooks: hooks:
- id: trailing-whitespace - id: trailing-whitespace
# - id: ... # - id: ...
```
### Hooks available ### Hooks available
#### `check-added-large-files` #### `check-added-large-files`
Prevent giant files from being committed. Prevent giant files from being committed.
- Specify what is "too large" with `args: ['--maxkb=123']` (default=500kB). - Specify what is "too large" with `args: ['--maxkb=123']` (default=500kB).
- Limits checked files to those indicated as staged for addition by git.
- If `git-lfs` is installed, lfs files will be skipped - If `git-lfs` is installed, lfs files will be skipped
(requires `git-lfs>=2.2.1`) (requires `git-lfs>=2.2.1`)
- `--enforce-all` - Check all listed files not just those staged for
addition.
#### `check-ast` #### `check-ast`
Simply check whether files parse as valid python. Simply check whether files parse as valid python.
@ -42,24 +38,23 @@ Require literal syntax when initializing empty or zero Python builtin types.
- Ignore this requirement for specific builtin types with `--ignore=type1,type2,…`. - Ignore this requirement for specific builtin types with `--ignore=type1,type2,…`.
- Forbid `dict` keyword syntax with `--no-allow-dict-kwargs`. - Forbid `dict` keyword syntax with `--no-allow-dict-kwargs`.
#### `check-byte-order-marker`
Forbid files which have a UTF-8 byte-order marker
#### `check-case-conflict` #### `check-case-conflict`
Check for files with names that would conflict on a case-insensitive filesystem like MacOS HFS+ or Windows FAT. Check for files with names that would conflict on a case-insensitive filesystem like MacOS HFS+ or Windows FAT.
#### `check-docstring-first`
Checks for a common error of placing code before the docstring.
#### `check-executables-have-shebangs` #### `check-executables-have-shebangs`
Checks that non-binary executables have a proper shebang. Checks that non-binary executables have a proper shebang.
#### `check-illegal-windows-names`
Check for files that cannot be created on Windows.
#### `check-json` #### `check-json`
Attempts to load all json files to verify syntax. Attempts to load all json files to verify syntax.
#### `check-merge-conflict` #### `check-merge-conflict`
Check for files that contain merge conflict strings. Check for files that contain merge conflict strings.
- `--assume-in-merge` - Allows running the hook when there is no ongoing merge operation
#### `check-shebang-scripts-are-executable`
Checks that scripts with shebangs are executable.
#### `check-symlinks` #### `check-symlinks`
Checks for symlinks which do not point to anything. Checks for symlinks which do not point to anything.
@ -69,10 +64,6 @@ Attempts to load all TOML files to verify syntax.
#### `check-vcs-permalinks` #### `check-vcs-permalinks`
Ensures that links to vcs websites are permalinks. Ensures that links to vcs websites are permalinks.
- `--additional-github-domain DOMAIN` - Add check for specified domain.
Can be repeated multiple times. for example, if your company uses
GitHub Enterprise you may use something like
`--additional-github-domain github.example.com`
#### `check-xml` #### `check-xml`
Attempts to load all xml files to verify syntax. Attempts to load all xml files to verify syntax.
@ -90,12 +81,6 @@ Attempts to load all yaml files to verify syntax.
#### `debug-statements` #### `debug-statements`
Check for debugger imports and py37+ `breakpoint()` calls in python source. Check for debugger imports and py37+ `breakpoint()` calls in python source.
#### `destroyed-symlinks`
Detects symlinks which are changed to regular files with a content of a path
which that symlink was pointing to.
This usually happens on Windows when a user clones a repository that has
symlinks but they do not have the permission to create symlinks.
#### `detect-aws-credentials` #### `detect-aws-credentials`
Checks for the existence of AWS secrets that you have set up with the AWS CLI. Checks for the existence of AWS secrets that you have set up with the AWS CLI.
The following arguments are available: The following arguments are available:
@ -113,28 +98,18 @@ This hook replaces double quoted strings with single quoted strings.
#### `end-of-file-fixer` #### `end-of-file-fixer`
Makes sure files end in a newline and only a newline. Makes sure files end in a newline and only a newline.
#### `fix-encoding-pragma`
Add `# -*- coding: utf-8 -*-` to the top of python files.
- To remove the coding pragma pass `--remove` (useful in a python3-only codebase)
#### `file-contents-sorter` #### `file-contents-sorter`
Sort the lines in specified files (defaults to alphabetical). Sort the lines in specified files (defaults to alphabetical).
You must provide the target [`files`](https://pre-commit.com/#config-files) as input. You must provide list of target files as input to it.
Note that this hook WILL remove blank lines and does NOT respect any comments. Note that this hook WILL remove blank lines and does NOT respect any comments.
All newlines will be converted to line feeds (`\n`).
The following arguments are available:
- `--ignore-case` - fold lower case to upper case characters.
- `--unique` - ensure each line is unique.
#### `fix-byte-order-marker`
removes UTF-8 byte order marker
#### `forbid-new-submodules` #### `forbid-new-submodules`
Prevent addition of new git submodules. Prevent addition of new git submodules.
This is intended as a helper to migrate away from submodules. If you want to
ban them entirely use `forbid-submodules`
#### `forbid-submodules`
forbids any submodules in the repository.
#### `mixed-line-ending` #### `mixed-line-ending`
Replaces or checks mixed line ending. Replaces or checks mixed line ending.
- `--fix={auto,crlf,lf,no}` - `--fix={auto,crlf,lf,no}`
@ -144,46 +119,37 @@ Replaces or checks mixed line ending.
- `no` - Checks if there is any mixed line ending without modifying any file. - `no` - Checks if there is any mixed line ending without modifying any file.
#### `name-tests-test` #### `name-tests-test`
verifies that test files are named correctly. Assert that files in tests/ end in `_test.py`.
- `--pytest` (the default): ensure tests match `.*_test\.py` - Use `args: ['--django']` to match `test*.py` instead.
- `--pytest-test-first`: ensure tests match `test_.*\.py`
- `--django` / `--unittest`: ensure tests match `test.*\.py`
#### `no-commit-to-branch` #### `no-commit-to-branch`
Protect specific branches from direct checkins. Protect specific branches from direct checkins.
- Use `args: [--branch, staging, --branch, main]` to set the branch. - Use `args: [--branch, staging, --branch, master]` to set the branch.
Both `main` and `master` are protected by default if no branch argument is set. `master` is the default if no branch argument is set.
- `-b` / `--branch` may be specified multiple times to protect multiple - `-b` / `--branch` may be specified multiple times to protect multiple
branches. branches.
- `-p` / `--pattern` can be used to protect branches that match a supplied regex - `-p` / `--pattern` can be used to protect branches that match a supplied regex
(e.g. `--pattern, release/.*`). May be specified multiple times. (e.g. `--pattern, release/.*`). May be specified multiple times.
Note that `no-commit-to-branch` is configured by default to [`always_run`](https://pre-commit.com/#config-always_run).
As a result, it will ignore any setting of [`files`](https://pre-commit.com/#config-files),
[`exclude`](https://pre-commit.com/#config-exclude), [`types`](https://pre-commit.com/#config-types)
or [`exclude_types`](https://pre-commit.com/#config-exclude_types).
Set [`always_run: false`](https://pre-commit.com/#config-always_run) to allow this hook to be skipped according to these
file filters. Caveat: In this configuration, empty commits (`git commit --allow-empty`) would always be allowed by this hook.
#### `pretty-format-json` #### `pretty-format-json`
Checks that all your JSON files are pretty. "Pretty" Checks that all your JSON files are pretty. "Pretty"
here means that keys are sorted and indented. You can configure this with here means that keys are sorted and indented. You can configure this with
the following commandline options: the following commandline options:
- `--autofix` - automatically format json files - `--autofix` - automatically format json files
- `--indent ...` - Control the indentation (either a number for a number of spaces or a string of whitespace). Defaults to 2 spaces. - `--indent ...` - Control the indentation (either a number for a number of spaces or a string of whitespace). Defaults to 4 spaces.
- `--no-ensure-ascii` preserve unicode characters instead of converting to escape sequences - `--no-ensure-ascii` preserve unicode characters instead of converting to escape sequences
- `--no-sort-keys` - when autofixing, retain the original key ordering (instead of sorting the keys) - `--no-sort-keys` - when autofixing, retain the original key ordering (instead of sorting the keys)
- `--top-keys comma,separated,keys` - Keys to keep at the top of mappings. - `--top-keys comma,separated,keys` - Keys to keep at the top of mappings.
#### `requirements-txt-fixer` #### `requirements-txt-fixer`
Sorts entries in requirements.txt and constraints.txt and removes incorrect entry for `pkg-resources==0.0.0` Sorts entries in requirements.txt and removes incorrect entry for `pkg-resources==0.0.0`
#### `sort-simple-yaml` #### `sort-simple-yaml`
Sorts simple YAML files which consist only of top-level Sorts simple YAML files which consist only of top-level
keys, preserving comments and blocks. keys, preserving comments and blocks.
Note that `sort-simple-yaml` by default matches no `files` as it enforces a Note that `sort-simple-yaml` by default matches no `files` as it enforces a
very specific format. You must opt in to this by setting [`files`](https://pre-commit.com/#config-files), for example: very specific format. You must opt in to this by setting `files`, for example:
```yaml ```yaml
- id: sort-simple-yaml - id: sort-simple-yaml
@ -202,10 +168,10 @@ Trims trailing whitespace.
### Deprecated / replaced hooks ### Deprecated / replaced hooks
- `check-byte-order-marker`: instead use fix-byte-order-marker - `autopep8-wrapper`: instead use
- `fix-encoding-pragma`: instead use [`pyupgrade`](https://github.com/asottile/pyupgrade) [mirrors-autopep8](https://github.com/pre-commit/mirrors-autopep8)
- `check-docstring-first`: fundamentally flawed, deprecated without replacement. - `pyflakes`: instead use `flake8`
- `flake8`: instead use [upstream flake8](https://gitlab.com/pycqa/flake8)
### As a standalone package ### As a standalone package

24
azure-pipelines.yml Normal file
View file

@ -0,0 +1,24 @@
trigger:
branches:
include: [master, test-me-*]
tags:
include: ['*']
resources:
repositories:
- repository: asottile
type: github
endpoint: github
name: asottile/azure-pipeline-templates
ref: refs/tags/v1.0.0
jobs:
- template: job--pre-commit.yml@asottile
- template: job--python-tox.yml@asottile
parameters:
toxenvs: [py27, py37]
os: windows
- template: job--python-tox.yml@asottile
parameters:
toxenvs: [pypy, pypy3, py27, py36, py37]
os: linux

View file

@ -0,0 +1,14 @@
from __future__ import absolute_import
from __future__ import print_function
from __future__ import unicode_literals
def main(): # type: () -> int
raise SystemExit(
'autopep8-wrapper is deprecated. Instead use autopep8 directly via '
'https://github.com/pre-commit/mirrors-autopep8',
)
if __name__ == '__main__':
exit(main())

View file

@ -1,81 +1,62 @@
from __future__ import annotations from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
import argparse import argparse
import json
import math import math
import os import os
import subprocess from typing import Iterable
from collections.abc import Sequence from typing import Optional
from typing import Sequence
from typing import Set
from pre_commit_hooks.util import added_files from pre_commit_hooks.util import added_files
from pre_commit_hooks.util import zsplit from pre_commit_hooks.util import CalledProcessError
from pre_commit_hooks.util import cmd_output
def filter_lfs_files(filenames: set[str]) -> None: # pragma: no cover (lfs) def lfs_files(): # type: () -> Set[str]
"""Remove files tracked by git-lfs from the set.""" try:
if not filenames: # Introduced in git-lfs 2.2.0, first working in 2.2.1
return lfs_ret = cmd_output('git', 'lfs', 'status', '--json')
except CalledProcessError: # pragma: no cover (with git-lfs)
lfs_ret = '{"files":{}}'
check_attr = subprocess.run( return set(json.loads(lfs_ret)['files'])
('git', 'check-attr', 'filter', '-z', '--stdin'),
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
encoding='utf-8',
check=True,
input='\0'.join(filenames),
)
stdout = zsplit(check_attr.stdout)
for i in range(0, len(stdout), 3):
filename, filter_tag = stdout[i], stdout[i + 2]
if filter_tag == 'lfs':
filenames.remove(filename)
def find_large_added_files( def find_large_added_files(filenames, maxkb):
filenames: Sequence[str], # type: (Iterable[str], int) -> int
maxkb: int,
*,
enforce_all: bool = False,
) -> int:
# Find all added files that are also in the list of files pre-commit tells # Find all added files that are also in the list of files pre-commit tells
# us about # us about
filenames = (added_files() & set(filenames)) - lfs_files()
retv = 0 retv = 0
filenames_filtered = set(filenames) for filename in filenames:
filter_lfs_files(filenames_filtered) kb = int(math.ceil(os.stat(filename).st_size / 1024))
if not enforce_all:
filenames_filtered &= added_files()
for filename in filenames_filtered:
kb = math.ceil(os.stat(filename).st_size / 1024)
if kb > maxkb: if kb > maxkb:
print(f'{filename} ({kb} KB) exceeds {maxkb} KB.') print('{} ({} KB) exceeds {} KB.'.format(filename, kb, maxkb))
retv = 1 retv = 1
return retv return retv
def main(argv: Sequence[str] | None = None) -> int: def main(argv=None): # type: (Optional[Sequence[str]]) -> int
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument( parser.add_argument(
'filenames', nargs='*', 'filenames', nargs='*',
help='Filenames pre-commit believes are changed.', help='Filenames pre-commit believes are changed.',
) )
parser.add_argument(
'--enforce-all', action='store_true',
help='Enforce all files are checked, not just staged files.',
)
parser.add_argument( parser.add_argument(
'--maxkb', type=int, default=500, '--maxkb', type=int, default=500,
help='Maximum allowable KB for added files', help='Maxmimum allowable KB for added files',
) )
args = parser.parse_args(argv)
return find_large_added_files( args = parser.parse_args(argv)
args.filenames, return find_large_added_files(args.filenames, args.maxkb)
args.maxkb,
enforce_all=args.enforce_all,
)
if __name__ == '__main__': if __name__ == '__main__':
raise SystemExit(main()) exit(main())

View file

@ -1,14 +1,17 @@
from __future__ import annotations from __future__ import absolute_import
from __future__ import print_function
from __future__ import unicode_literals
import argparse import argparse
import ast import ast
import platform import platform
import sys import sys
import traceback import traceback
from collections.abc import Sequence from typing import Optional
from typing import Sequence
def main(argv: Sequence[str] | None = None) -> int: def main(argv=None): # type: (Optional[Sequence[str]]) -> int
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument('filenames', nargs='*') parser.add_argument('filenames', nargs='*')
args = parser.parse_args(argv) args = parser.parse_args(argv)
@ -20,14 +23,18 @@ def main(argv: Sequence[str] | None = None) -> int:
with open(filename, 'rb') as f: with open(filename, 'rb') as f:
ast.parse(f.read(), filename=filename) ast.parse(f.read(), filename=filename)
except SyntaxError: except SyntaxError:
impl = platform.python_implementation() print(
version = sys.version.split()[0] '{}: failed parsing with {} {}:'.format(
print(f'{filename}: failed parsing with {impl} {version}:') filename,
platform.python_implementation(),
sys.version.partition(' ')[0],
),
)
tb = ' ' + traceback.format_exc().replace('\n', '\n ') tb = ' ' + traceback.format_exc().replace('\n', '\n ')
print(f'\n{tb}') print('\n{}'.format(tb))
retval = 1 retval = 1
return retval return retval
if __name__ == '__main__': if __name__ == '__main__':
raise SystemExit(main()) exit(main())

View file

@ -1,9 +1,13 @@
from __future__ import annotations from __future__ import unicode_literals
import argparse import argparse
import ast import ast
from collections.abc import Sequence import collections
from typing import NamedTuple import sys
from typing import List
from typing import Optional
from typing import Sequence
from typing import Set
BUILTIN_TYPES = { BUILTIN_TYPES = {
@ -17,48 +21,41 @@ BUILTIN_TYPES = {
} }
class Call(NamedTuple): Call = collections.namedtuple('Call', ['name', 'line', 'column'])
name: str
line: int
column: int
class Visitor(ast.NodeVisitor): class Visitor(ast.NodeVisitor):
def __init__( def __init__(self, ignore=None, allow_dict_kwargs=True):
self, # type: (Optional[Sequence[str]], bool) -> None
ignore: set[str], self.builtin_type_calls = [] # type: List[Call]
allow_dict_kwargs: bool = True, self.ignore = set(ignore) if ignore else set()
) -> None:
self.builtin_type_calls: list[Call] = []
self.allow_dict_kwargs = allow_dict_kwargs self.allow_dict_kwargs = allow_dict_kwargs
self._disallowed = BUILTIN_TYPES.keys() - ignore
def _check_dict_call(self, node: ast.Call) -> bool: def _check_dict_call(self, node): # type: (ast.Call) -> bool
return self.allow_dict_kwargs and bool(node.keywords) return (
self.allow_dict_kwargs and
(getattr(node, 'kwargs', None) or getattr(node, 'keywords', None))
)
def visit_Call(self, node: ast.Call) -> None: def visit_Call(self, node): # type: (ast.Call) -> None
if ( if not isinstance(node.func, ast.Name):
# Ignore functions that are object attributes (`foo.bar()`). # Ignore functions that are object attributes (`foo.bar()`).
# Assume that if the user calls `builtins.list()`, they know what # Assume that if the user calls `builtins.list()`, they know what
# they're doing. # they're doing.
isinstance(node.func, ast.Name) and return
node.func.id in self._disallowed and if node.func.id not in set(BUILTIN_TYPES).difference(self.ignore):
(node.func.id != 'dict' or not self._check_dict_call(node)) and return
not node.args if node.func.id == 'dict' and self._check_dict_call(node):
): return
elif node.args:
return
self.builtin_type_calls.append( self.builtin_type_calls.append(
Call(node.func.id, node.lineno, node.col_offset), Call(node.func.id, node.lineno, node.col_offset),
) )
self.generic_visit(node)
def check_file(filename, ignore=None, allow_dict_kwargs=True):
def check_file( # type: (str, Optional[Sequence[str]], bool) -> List[Call]
filename: str,
*,
ignore: set[str],
allow_dict_kwargs: bool = True,
) -> list[Call]:
with open(filename, 'rb') as f: with open(filename, 'rb') as f:
tree = ast.parse(f.read(), filename=filename) tree = ast.parse(f.read(), filename=filename)
visitor = Visitor(ignore=ignore, allow_dict_kwargs=allow_dict_kwargs) visitor = Visitor(ignore=ignore, allow_dict_kwargs=allow_dict_kwargs)
@ -66,11 +63,11 @@ def check_file(
return visitor.builtin_type_calls return visitor.builtin_type_calls
def parse_ignore(value: str) -> set[str]: def parse_ignore(value): # type: (str) -> Set[str]
return set(value.split(',')) return set(value.split(','))
def main(argv: Sequence[str] | None = None) -> int: def main(argv=None): # type: (Optional[Sequence[str]]) -> int
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument('filenames', nargs='*') parser.add_argument('filenames', nargs='*')
parser.add_argument('--ignore', type=parse_ignore, default=set()) parser.add_argument('--ignore', type=parse_ignore, default=set())
@ -96,11 +93,15 @@ def main(argv: Sequence[str] | None = None) -> int:
rc = rc or 1 rc = rc or 1
for call in calls: for call in calls:
print( print(
f'{filename}:{call.line}:{call.column}: ' '{filename}:{call.line}:{call.column}: '
f'replace {call.name}() with {BUILTIN_TYPES[call.name]}', 'replace {call.name}() with {replacement}'.format(
filename=filename,
call=call,
replacement=BUILTIN_TYPES[call.name],
),
) )
return rc return rc
if __name__ == '__main__': if __name__ == '__main__':
raise SystemExit(main()) sys.exit(main())

View file

@ -0,0 +1,27 @@
from __future__ import absolute_import
from __future__ import print_function
from __future__ import unicode_literals
import argparse
from typing import Optional
from typing import Sequence
def main(argv=None): # type: (Optional[Sequence[str]]) -> int
parser = argparse.ArgumentParser()
parser.add_argument('filenames', nargs='*', help='Filenames to check')
args = parser.parse_args(argv)
retv = 0
for filename in args.filenames:
with open(filename, 'rb') as f:
if f.read(3) == b'\xef\xbb\xbf':
retv = 1
print('{}: Has a byte-order marker'.format(filename))
return retv
if __name__ == '__main__':
exit(main())

View file

@ -1,35 +1,24 @@
from __future__ import annotations from __future__ import absolute_import
from __future__ import print_function
from __future__ import unicode_literals
import argparse import argparse
from collections.abc import Iterable from typing import Iterable
from collections.abc import Iterator from typing import Optional
from collections.abc import Sequence from typing import Sequence
from typing import Set
from pre_commit_hooks.util import added_files from pre_commit_hooks.util import added_files
from pre_commit_hooks.util import cmd_output from pre_commit_hooks.util import cmd_output
def lower_set(iterable: Iterable[str]) -> set[str]: def lower_set(iterable): # type: (Iterable[str]) -> Set[str]
return {x.lower() for x in iterable} return {x.lower() for x in iterable}
def parents(file: str) -> Iterator[str]: def find_conflicting_filenames(filenames): # type: (Sequence[str]) -> int
path_parts = file.split('/')
path_parts.pop()
while path_parts:
yield '/'.join(path_parts)
path_parts.pop()
def directories_for(files: set[str]) -> set[str]:
return {parent for file in files for parent in parents(file)}
def find_conflicting_filenames(filenames: Sequence[str]) -> int:
repo_files = set(cmd_output('git', 'ls-files').splitlines()) repo_files = set(cmd_output('git', 'ls-files').splitlines())
repo_files |= directories_for(repo_files)
relevant_files = set(filenames) | added_files() relevant_files = set(filenames) | added_files()
relevant_files |= directories_for(relevant_files)
repo_files -= relevant_files repo_files -= relevant_files
retv = 0 retv = 0
@ -50,13 +39,13 @@ def find_conflicting_filenames(filenames: Sequence[str]) -> int:
if x.lower() in conflicts if x.lower() in conflicts
] ]
for filename in sorted(conflicting_files): for filename in sorted(conflicting_files):
print(f'Case-insensitivity conflict found: {filename}') print('Case-insensitivity conflict found: {}'.format(filename))
retv = 1 retv = 1
return retv return retv
def main(argv: Sequence[str] | None = None) -> int: def main(argv=None): # type: (Optional[Sequence[str]]) -> int
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument( parser.add_argument(
'filenames', nargs='*', 'filenames', nargs='*',
@ -69,4 +58,4 @@ def main(argv: Sequence[str] | None = None) -> int:
if __name__ == '__main__': if __name__ == '__main__':
raise SystemExit(main()) exit(main())

View file

@ -1,18 +1,30 @@
from __future__ import annotations from __future__ import absolute_import
from __future__ import print_function
from __future__ import unicode_literals
import argparse import argparse
import io import io
import tokenize import tokenize
from collections.abc import Sequence from typing import Optional
from tokenize import tokenize as tokenize_tokenize from typing import Sequence
NON_CODE_TOKENS = frozenset(( import six
tokenize.COMMENT, tokenize.ENDMARKER, tokenize.NEWLINE, tokenize.NL,
tokenize.ENCODING, if six.PY2: # pragma: no cover (PY2)
)) from tokenize import generate_tokens as tokenize_tokenize
OTHER_NON_CODE = ()
else: # pragma: no cover (PY3)
from tokenize import tokenize as tokenize_tokenize
OTHER_NON_CODE = (tokenize.ENCODING,)
NON_CODE_TOKENS = frozenset(
(tokenize.COMMENT, tokenize.ENDMARKER, tokenize.NEWLINE, tokenize.NL) +
OTHER_NON_CODE,
)
def check_docstring_first(src: bytes, filename: str = '<unknown>') -> int: def check_docstring_first(src, filename='<unknown>'):
# type: (bytes, str) -> int
"""Returns nonzero if the source has what looks like a docstring that is """Returns nonzero if the source has what looks like a docstring that is
not at the beginning of the source. not at the beginning of the source.
@ -28,14 +40,18 @@ def check_docstring_first(src: bytes, filename: str = '<unknown>') -> int:
if tok_type == tokenize.STRING and scol == 0: if tok_type == tokenize.STRING and scol == 0:
if found_docstring_line is not None: if found_docstring_line is not None:
print( print(
f'{filename}:{sline}: Multiple module docstrings ' '{}:{} Multiple module docstrings '
f'(first docstring on line {found_docstring_line}).', '(first docstring on line {}).'.format(
filename, sline, found_docstring_line,
),
) )
return 1 return 1
elif found_code_line is not None: elif found_code_line is not None:
print( print(
f'{filename}:{sline}: Module docstring appears after code ' '{}:{} Module docstring appears after code '
f'(code seen on line {found_code_line}).', '(code seen on line {}).'.format(
filename, sline, found_code_line,
),
) )
return 1 return 1
else: else:
@ -46,7 +62,7 @@ def check_docstring_first(src: bytes, filename: str = '<unknown>') -> int:
return 0 return 0
def main(argv: Sequence[str] | None = None) -> int: def main(argv=None): # type: (Optional[Sequence[str]]) -> int
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument('filenames', nargs='*') parser.add_argument('filenames', nargs='*')
args = parser.parse_args(argv) args = parser.parse_args(argv)

View file

@ -1,85 +1,47 @@
"""Check that executable text files have a shebang.""" """Check that executable text files have a shebang."""
from __future__ import annotations from __future__ import absolute_import
from __future__ import print_function
from __future__ import unicode_literals
import argparse import argparse
import shlex import pipes
import sys import sys
from collections.abc import Generator from typing import Optional
from collections.abc import Sequence from typing import Sequence
from typing import NamedTuple
from pre_commit_hooks.util import cmd_output
from pre_commit_hooks.util import zsplit
EXECUTABLE_VALUES = frozenset(('1', '3', '5', '7'))
def check_executables(paths: list[str]) -> int: def check_has_shebang(path): # type: (str) -> int
fs_tracks_executable_bit = cmd_output(
'git', 'config', 'core.fileMode', retcode=None,
).strip()
if fs_tracks_executable_bit == 'false': # pragma: win32 cover
return _check_git_filemode(paths)
else: # pragma: win32 no cover
retv = 0
for path in paths:
if not has_shebang(path):
_message(path)
retv = 1
return retv
class GitLsFile(NamedTuple):
mode: str
filename: str
def git_ls_files(paths: Sequence[str]) -> Generator[GitLsFile]:
outs = cmd_output('git', 'ls-files', '-z', '--stage', '--', *paths)
for out in zsplit(outs):
metadata, filename = out.split('\t')
mode, _, _ = metadata.split()
yield GitLsFile(mode, filename)
def _check_git_filemode(paths: Sequence[str]) -> int:
seen: set[str] = set()
for ls_file in git_ls_files(paths):
is_executable = any(b in EXECUTABLE_VALUES for b in ls_file.mode[-3:])
if is_executable and not has_shebang(ls_file.filename):
_message(ls_file.filename)
seen.add(ls_file.filename)
return int(bool(seen))
def has_shebang(path: str) -> int:
with open(path, 'rb') as f: with open(path, 'rb') as f:
first_bytes = f.read(2) first_bytes = f.read(2)
return first_bytes == b'#!' if first_bytes != b'#!':
def _message(path: str) -> None:
print( print(
f'{path}: marked executable but has no (or invalid) shebang!\n' '{path}: marked executable but has no (or invalid) shebang!\n'
f" If it isn't supposed to be executable, try: " " If it isn't supposed to be executable, try: chmod -x {quoted}\n"
f'`chmod -x {shlex.quote(path)}`\n' ' If it is supposed to be executable, double-check its shebang.'
f' If on Windows, you may also need to: ' .format(
f'`git add --chmod=-x {shlex.quote(path)}`\n' path=path,
f' If it is supposed to be executable, double-check its shebang.', quoted=pipes.quote(path),
),
file=sys.stderr, file=sys.stderr,
) )
return 1
else:
return 0
def main(argv: Sequence[str] | None = None) -> int: def main(argv=None): # type: (Optional[Sequence[str]]) -> int
parser = argparse.ArgumentParser(description=__doc__) parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('filenames', nargs='*') parser.add_argument('filenames', nargs='*')
args = parser.parse_args(argv) args = parser.parse_args(argv)
return check_executables(args.filenames) retv = 0
for filename in args.filenames:
retv |= check_has_shebang(filename)
return retv
if __name__ == '__main__': if __name__ == '__main__':
raise SystemExit(main()) exit(main())

View file

@ -1,38 +1,27 @@
from __future__ import annotations from __future__ import print_function
import argparse import argparse
import io
import json import json
from collections.abc import Sequence import sys
from typing import Any from typing import Optional
from typing import Sequence
def raise_duplicate_keys( def main(argv=None): # type: (Optional[Sequence[str]]) -> int
ordered_pairs: list[tuple[str, Any]],
) -> dict[str, Any]:
d = {}
for key, val in ordered_pairs:
if key in d:
raise ValueError(f'Duplicate key: {key}')
else:
d[key] = val
return d
def main(argv: Sequence[str] | None = None) -> int:
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument('filenames', nargs='*', help='Filenames to check.') parser.add_argument('filenames', nargs='*', help='Filenames to check.')
args = parser.parse_args(argv) args = parser.parse_args(argv)
retval = 0 retval = 0
for filename in args.filenames: for filename in args.filenames:
with open(filename, 'rb') as f:
try: try:
json.load(f, object_pairs_hook=raise_duplicate_keys) json.load(io.open(filename, encoding='UTF-8'))
except ValueError as exc: except (ValueError, UnicodeDecodeError) as exc:
print(f'{filename}: Failed to json decode ({exc})') print('{}: Failed to json decode ({})'.format(filename, exc))
retval = 1 retval = 1
return retval return retval
if __name__ == '__main__': if __name__ == '__main__':
raise SystemExit(main()) sys.exit(main())

View file

@ -1,34 +1,32 @@
from __future__ import annotations from __future__ import print_function
import argparse import argparse
import os.path import os.path
from collections.abc import Sequence from typing import Optional
from typing import Sequence
from pre_commit_hooks.util import cmd_output
CONFLICT_PATTERNS = [ CONFLICT_PATTERNS = [
b'<<<<<<< ', b'<<<<<<< ',
b'======= ', b'======= ',
b'=======\r\n',
b'=======\n', b'=======\n',
b'>>>>>>> ', b'>>>>>>> ',
] ]
WARNING_MSG = 'Merge conflict string "{0}" found in {1}:{2}'
def is_in_merge() -> bool: def is_in_merge(): # type: () -> int
git_dir = cmd_output('git', 'rev-parse', '--git-dir').rstrip()
return ( return (
os.path.exists(os.path.join(git_dir, 'MERGE_MSG')) and os.path.exists(os.path.join('.git', 'MERGE_MSG')) and
( (
os.path.exists(os.path.join(git_dir, 'MERGE_HEAD')) or os.path.exists(os.path.join('.git', 'MERGE_HEAD')) or
os.path.exists(os.path.join(git_dir, 'rebase-apply')) or os.path.exists(os.path.join('.git', 'rebase-apply')) or
os.path.exists(os.path.join(git_dir, 'rebase-merge')) os.path.exists(os.path.join('.git', 'rebase-merge'))
) )
) )
def main(argv: Sequence[str] | None = None) -> int: def main(argv=None): # type: (Optional[Sequence[str]]) -> int
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument('filenames', nargs='*') parser.add_argument('filenames', nargs='*')
parser.add_argument('--assume-in-merge', action='store_true') parser.add_argument('--assume-in-merge', action='store_true')
@ -40,12 +38,13 @@ def main(argv: Sequence[str] | None = None) -> int:
retcode = 0 retcode = 0
for filename in args.filenames: for filename in args.filenames:
with open(filename, 'rb') as inputfile: with open(filename, 'rb') as inputfile:
for i, line in enumerate(inputfile, start=1): for i, line in enumerate(inputfile):
for pattern in CONFLICT_PATTERNS: for pattern in CONFLICT_PATTERNS:
if line.startswith(pattern): if line.startswith(pattern):
print( print(
f'{filename}:{i}: Merge conflict string ' WARNING_MSG.format(
f'{pattern.strip().decode()!r} found', pattern.decode(), filename, i + 1,
),
) )
retcode = 1 retcode = 1
@ -53,4 +52,4 @@ def main(argv: Sequence[str] | None = None) -> int:
if __name__ == '__main__': if __name__ == '__main__':
raise SystemExit(main()) exit(main())

View file

@ -1,54 +0,0 @@
"""Check that text files with a shebang are executable."""
from __future__ import annotations
import argparse
import shlex
import sys
from collections.abc import Sequence
from pre_commit_hooks.check_executables_have_shebangs import EXECUTABLE_VALUES
from pre_commit_hooks.check_executables_have_shebangs import git_ls_files
from pre_commit_hooks.check_executables_have_shebangs import has_shebang
def check_shebangs(paths: list[str]) -> int:
# Cannot optimize on non-executability here if we intend this check to
# work on win32 -- and that's where problems caused by non-executability
# (elsewhere) are most likely to arise from.
return _check_git_filemode(paths)
def _check_git_filemode(paths: Sequence[str]) -> int:
seen: set[str] = set()
for ls_file in git_ls_files(paths):
is_executable = any(b in EXECUTABLE_VALUES for b in ls_file.mode[-3:])
if not is_executable and has_shebang(ls_file.filename):
_message(ls_file.filename)
seen.add(ls_file.filename)
return int(bool(seen))
def _message(path: str) -> None:
print(
f'{path}: has a shebang but is not marked executable!\n'
f' If it is supposed to be executable, try: '
f'`chmod +x {shlex.quote(path)}`\n'
f' If on Windows, you may also need to: '
f'`git add --chmod=+x {shlex.quote(path)}`\n'
f' If it is not supposed to be executable, double-check its shebang '
f'is wanted.\n',
file=sys.stderr,
)
def main(argv: Sequence[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('filenames', nargs='*')
args = parser.parse_args(argv)
return check_shebangs(args.filenames)
if __name__ == '__main__':
raise SystemExit(main())

View file

@ -1,11 +1,14 @@
from __future__ import annotations from __future__ import absolute_import
from __future__ import print_function
from __future__ import unicode_literals
import argparse import argparse
import os.path import os.path
from collections.abc import Sequence from typing import Optional
from typing import Sequence
def main(argv: Sequence[str] | None = None) -> int: def main(argv=None): # type: (Optional[Sequence[str]]) -> int
parser = argparse.ArgumentParser(description='Checks for broken symlinks.') parser = argparse.ArgumentParser(description='Checks for broken symlinks.')
parser.add_argument('filenames', nargs='*', help='Filenames to check') parser.add_argument('filenames', nargs='*', help='Filenames to check')
args = parser.parse_args(argv) args = parser.parse_args(argv)
@ -17,11 +20,11 @@ def main(argv: Sequence[str] | None = None) -> int:
os.path.islink(filename) and os.path.islink(filename) and
not os.path.exists(filename) not os.path.exists(filename)
): # pragma: no cover (symlink support required) ): # pragma: no cover (symlink support required)
print(f'{filename}: Broken symlink') print('{}: Broken symlink'.format(filename))
retv = 1 retv = 1
return retv return retv
if __name__ == '__main__': if __name__ == '__main__':
raise SystemExit(main()) exit(main())

View file

@ -1,16 +1,14 @@
from __future__ import annotations from __future__ import print_function
import argparse import argparse
import sys import sys
from collections.abc import Sequence from typing import Optional
from typing import Sequence
if sys.version_info >= (3, 11): # pragma: >=3.11 cover import toml
import tomllib
else: # pragma: <3.11 cover
import tomli as tomllib
def main(argv: Sequence[str] | None = None) -> int: def main(argv=None): # type: (Optional[Sequence[str]]) -> int
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument('filenames', nargs='*', help='Filenames to check.') parser.add_argument('filenames', nargs='*', help='Filenames to check.')
args = parser.parse_args(argv) args = parser.parse_args(argv)
@ -18,13 +16,13 @@ def main(argv: Sequence[str] | None = None) -> int:
retval = 0 retval = 0
for filename in args.filenames: for filename in args.filenames:
try: try:
with open(filename, mode='rb') as fp: with open(filename) as f:
tomllib.load(fp) toml.load(f)
except tomllib.TOMLDecodeError as exc: except toml.TomlDecodeError as exc:
print(f'{filename}: {exc}') print('{}: {}'.format(filename, exc))
retval = 1 retval = 1
return retval return retval
if __name__ == '__main__': if __name__ == '__main__':
raise SystemExit(main()) sys.exit(main())

View file

@ -1,53 +1,39 @@
from __future__ import annotations from __future__ import absolute_import
from __future__ import print_function
from __future__ import unicode_literals
import argparse import argparse
import re import re
import sys import sys
from collections.abc import Sequence from typing import Optional
from re import Pattern from typing import Sequence
def _get_pattern(domain: str) -> Pattern[bytes]: GITHUB_NON_PERMALINK = re.compile(
regex = ( br'https://github.com/[^/ ]+/[^/ ]+/blob/master/[^# ]+#L\d+',
rf'https://{domain}/[^/ ]+/[^/ ]+/blob/' )
r'(?![a-fA-F0-9]{4,64}/)([^/. ]+)/[^# ]+#L\d+'
)
return re.compile(regex.encode())
def _check_filename(filename: str, patterns: list[Pattern[bytes]]) -> int: def _check_filename(filename): # type: (str) -> int
retv = 0 retv = 0
with open(filename, 'rb') as f: with open(filename, 'rb') as f:
for i, line in enumerate(f, 1): for i, line in enumerate(f, 1):
for pattern in patterns: if GITHUB_NON_PERMALINK.search(line):
if pattern.search(line): sys.stdout.write('{}:{}:'.format(filename, i))
sys.stdout.write(f'{filename}:{i}:')
sys.stdout.flush() sys.stdout.flush()
sys.stdout.buffer.write(line) getattr(sys.stdout, 'buffer', sys.stdout).write(line)
retv = 1 retv = 1
return retv return retv
def main(argv: Sequence[str] | None = None) -> int: def main(argv=None): # type: (Optional[Sequence[str]]) -> int
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument('filenames', nargs='*') parser.add_argument('filenames', nargs='*')
parser.add_argument(
'--additional-github-domain',
dest='additional_github_domains',
action='append',
default=['github.com'],
)
args = parser.parse_args(argv) args = parser.parse_args(argv)
patterns = [
_get_pattern(domain)
for domain in args.additional_github_domains
]
retv = 0 retv = 0
for filename in args.filenames: for filename in args.filenames:
retv |= _check_filename(filename, patterns) retv |= _check_filename(filename)
if retv: if retv:
print() print()
@ -57,4 +43,4 @@ def main(argv: Sequence[str] | None = None) -> int:
if __name__ == '__main__': if __name__ == '__main__':
raise SystemExit(main()) exit(main())

View file

@ -1,26 +1,30 @@
from __future__ import annotations from __future__ import absolute_import
from __future__ import print_function
from __future__ import unicode_literals
import argparse import argparse
import io
import sys
import xml.sax.handler import xml.sax.handler
from collections.abc import Sequence from typing import Optional
from typing import Sequence
def main(argv: Sequence[str] | None = None) -> int: def main(argv=None): # type: (Optional[Sequence[str]]) -> int
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument('filenames', nargs='*', help='XML filenames to check.') parser.add_argument('filenames', nargs='*', help='XML filenames to check.')
args = parser.parse_args(argv) args = parser.parse_args(argv)
retval = 0 retval = 0
handler = xml.sax.handler.ContentHandler()
for filename in args.filenames: for filename in args.filenames:
try: try:
with open(filename, 'rb') as xml_file: with io.open(filename, 'rb') as xml_file:
xml.sax.parse(xml_file, handler) xml.sax.parse(xml_file, xml.sax.handler.ContentHandler())
except xml.sax.SAXException as exc: except xml.sax.SAXException as exc:
print(f'{filename}: Failed to xml parse ({exc})') print('{}: Failed to xml parse ({})'.format(filename, exc))
retval = 1 retval = 1
return retval return retval
if __name__ == '__main__': if __name__ == '__main__':
raise SystemExit(main()) sys.exit(main())

View file

@ -1,34 +1,33 @@
from __future__ import annotations from __future__ import print_function
import argparse import argparse
from collections.abc import Generator import collections
from collections.abc import Sequence import io
import sys
from typing import Any from typing import Any
from typing import NamedTuple from typing import Generator
from typing import Optional
from typing import Sequence
import ruamel.yaml import ruamel.yaml
yaml = ruamel.yaml.YAML(typ='safe') yaml = ruamel.yaml.YAML(typ='safe')
def _exhaust(gen: Generator[str]) -> None: def _exhaust(gen): # type: (Generator[str, None, None]) -> None
for _ in gen: for _ in gen:
pass pass
def _parse_unsafe(*args: Any, **kwargs: Any) -> None: def _parse_unsafe(*args, **kwargs): # type: (*Any, **Any) -> None
_exhaust(yaml.parse(*args, **kwargs)) _exhaust(yaml.parse(*args, **kwargs))
def _load_all(*args: Any, **kwargs: Any) -> None: def _load_all(*args, **kwargs): # type: (*Any, **Any) -> None
_exhaust(yaml.load_all(*args, **kwargs)) _exhaust(yaml.load_all(*args, **kwargs))
class Key(NamedTuple): Key = collections.namedtuple('Key', ('multi', 'unsafe'))
multi: bool
unsafe: bool
LOAD_FNS = { LOAD_FNS = {
Key(multi=False, unsafe=False): yaml.load, Key(multi=False, unsafe=False): yaml.load,
Key(multi=False, unsafe=True): _parse_unsafe, Key(multi=False, unsafe=True): _parse_unsafe,
@ -37,7 +36,7 @@ LOAD_FNS = {
} }
def main(argv: Sequence[str] | None = None) -> int: def main(argv=None): # type: (Optional[Sequence[str]]) -> int
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument( parser.add_argument(
'-m', '--multi', '--allow-multiple-documents', action='store_true', '-m', '--multi', '--allow-multiple-documents', action='store_true',
@ -46,7 +45,7 @@ def main(argv: Sequence[str] | None = None) -> int:
'--unsafe', action='store_true', '--unsafe', action='store_true',
help=( help=(
'Instead of loading the files, simply parse them for syntax. ' 'Instead of loading the files, simply parse them for syntax. '
'A syntax-only check enables extensions and unsafe constructs ' 'A syntax-only check enables extensions and unsafe contstructs '
'which would otherwise be forbidden. Using this option removes ' 'which would otherwise be forbidden. Using this option removes '
'all guarantees of portability to other yaml implementations. ' 'all guarantees of portability to other yaml implementations. '
'Implies --allow-multiple-documents' 'Implies --allow-multiple-documents'
@ -60,7 +59,7 @@ def main(argv: Sequence[str] | None = None) -> int:
retval = 0 retval = 0
for filename in args.filenames: for filename in args.filenames:
try: try:
with open(filename, encoding='UTF-8') as f: with io.open(filename, encoding='UTF-8') as f:
load_fn(f) load_fn(f)
except ruamel.yaml.YAMLError as exc: except ruamel.yaml.YAMLError as exc:
print(exc) print(exc)
@ -69,4 +68,4 @@ def main(argv: Sequence[str] | None = None) -> int:
if __name__ == '__main__': if __name__ == '__main__':
raise SystemExit(main()) sys.exit(main())

View file

@ -1,49 +1,35 @@
from __future__ import annotations from __future__ import print_function
from __future__ import unicode_literals
import argparse import argparse
import ast import ast
import collections
import traceback import traceback
from collections.abc import Sequence from typing import List
from typing import NamedTuple from typing import Optional
from typing import Sequence
DEBUG_STATEMENTS = { DEBUG_STATEMENTS = {'pdb', 'ipdb', 'pudb', 'q', 'rdb', 'rpdb'}
'bpdb', Debug = collections.namedtuple('Debug', ('line', 'col', 'name', 'reason'))
'ipdb',
'pdb',
'pdbr',
'pudb',
'pydevd_pycharm',
'q',
'rdb',
'rpdb',
'wdb',
}
class Debug(NamedTuple):
line: int
col: int
name: str
reason: str
class DebugStatementParser(ast.NodeVisitor): class DebugStatementParser(ast.NodeVisitor):
def __init__(self) -> None: def __init__(self): # type: () -> None
self.breakpoints: list[Debug] = [] self.breakpoints = [] # type: List[Debug]
def visit_Import(self, node: ast.Import) -> None: def visit_Import(self, node): # type: (ast.Import) -> None
for name in node.names: for name in node.names:
if name.name in DEBUG_STATEMENTS: if name.name in DEBUG_STATEMENTS:
st = Debug(node.lineno, node.col_offset, name.name, 'imported') st = Debug(node.lineno, node.col_offset, name.name, 'imported')
self.breakpoints.append(st) self.breakpoints.append(st)
def visit_ImportFrom(self, node: ast.ImportFrom) -> None: def visit_ImportFrom(self, node): # type: (ast.ImportFrom) -> None
if node.module in DEBUG_STATEMENTS: if node.module in DEBUG_STATEMENTS:
st = Debug(node.lineno, node.col_offset, node.module, 'imported') st = Debug(node.lineno, node.col_offset, node.module, 'imported')
self.breakpoints.append(st) self.breakpoints.append(st)
def visit_Call(self, node: ast.Call) -> None: def visit_Call(self, node): # type: (ast.Call) -> None
"""python3.7+ breakpoint()""" """python3.7+ breakpoint()"""
if isinstance(node.func, ast.Name) and node.func.id == 'breakpoint': if isinstance(node.func, ast.Name) and node.func.id == 'breakpoint':
st = Debug(node.lineno, node.col_offset, node.func.id, 'called') st = Debug(node.lineno, node.col_offset, node.func.id, 'called')
@ -51,12 +37,12 @@ class DebugStatementParser(ast.NodeVisitor):
self.generic_visit(node) self.generic_visit(node)
def check_file(filename: str) -> int: def check_file(filename): # type: (str) -> int
try: try:
with open(filename, 'rb') as f: with open(filename, 'rb') as f:
ast_obj = ast.parse(f.read(), filename=filename) ast_obj = ast.parse(f.read(), filename=filename)
except SyntaxError: except SyntaxError:
print(f'{filename} - Could not parse ast') print('{} - Could not parse ast'.format(filename))
print() print()
print('\t' + traceback.format_exc().replace('\n', '\n\t')) print('\t' + traceback.format_exc().replace('\n', '\n\t'))
print() print()
@ -66,12 +52,16 @@ def check_file(filename: str) -> int:
visitor.visit(ast_obj) visitor.visit(ast_obj)
for bp in visitor.breakpoints: for bp in visitor.breakpoints:
print(f'{filename}:{bp.line}:{bp.col}: {bp.name} {bp.reason}') print(
'{}:{}:{} - {} {}'.format(
filename, bp.line, bp.col, bp.name, bp.reason,
),
)
return int(bool(visitor.breakpoints)) return int(bool(visitor.breakpoints))
def main(argv: Sequence[str] | None = None) -> int: def main(argv=None): # type: (Optional[Sequence[str]]) -> int
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument('filenames', nargs='*', help='Filenames to run') parser.add_argument('filenames', nargs='*', help='Filenames to run')
args = parser.parse_args(argv) args = parser.parse_args(argv)
@ -83,4 +73,4 @@ def main(argv: Sequence[str] | None = None) -> int:
if __name__ == '__main__': if __name__ == '__main__':
raise SystemExit(main()) exit(main())

View file

@ -1,92 +0,0 @@
from __future__ import annotations
import argparse
import shlex
import subprocess
from collections.abc import Sequence
from pre_commit_hooks.util import cmd_output
from pre_commit_hooks.util import zsplit
ORDINARY_CHANGED_ENTRIES_MARKER = '1'
PERMS_LINK = '120000'
PERMS_NONEXIST = '000000'
def find_destroyed_symlinks(files: Sequence[str]) -> list[str]:
destroyed_links: list[str] = []
if not files:
return destroyed_links
for line in zsplit(
cmd_output('git', 'status', '--porcelain=v2', '-z', '--', *files),
):
splitted = line.split(' ')
if splitted and splitted[0] == ORDINARY_CHANGED_ENTRIES_MARKER:
# https://git-scm.com/docs/git-status#_changed_tracked_entries
(
_, _, _,
mode_HEAD,
mode_index,
_,
hash_HEAD,
hash_index,
*path_splitted,
) = splitted
path = ' '.join(path_splitted)
if (
mode_HEAD == PERMS_LINK and
mode_index != PERMS_LINK and
mode_index != PERMS_NONEXIST
):
if hash_HEAD == hash_index:
# if old and new hashes are equal, it's not needed to check
# anything more, we've found a destroyed symlink for sure
destroyed_links.append(path)
else:
# if old and new hashes are *not* equal, it doesn't mean
# that everything is OK - new file may be altered
# by something like trailing-whitespace and/or
# mixed-line-ending hooks so we need to go deeper
SIZE_CMD = ('git', 'cat-file', '-s')
size_index = int(cmd_output(*SIZE_CMD, hash_index).strip())
size_HEAD = int(cmd_output(*SIZE_CMD, hash_HEAD).strip())
# in the worst case new file may have CRLF added
# so check content only if new file is bigger
# not more than 2 bytes compared to the old one
if size_index <= size_HEAD + 2:
head_content = subprocess.check_output(
('git', 'cat-file', '-p', hash_HEAD),
).rstrip()
index_content = subprocess.check_output(
('git', 'cat-file', '-p', hash_index),
).rstrip()
if head_content == index_content:
destroyed_links.append(path)
return destroyed_links
def main(argv: Sequence[str] | None = None) -> int:
parser = argparse.ArgumentParser()
parser.add_argument('filenames', nargs='*', help='Filenames to check.')
args = parser.parse_args(argv)
destroyed_links = find_destroyed_symlinks(files=args.filenames)
if destroyed_links:
print('Destroyed symlinks:')
for destroyed_link in destroyed_links:
print(f'- {destroyed_link}')
print('You should unstage affected files:')
print(f'\tgit reset HEAD -- {shlex.join(destroyed_links)}')
print(
'And retry commit. As a long term solution '
'you may try to explicitly tell git that your '
'environment does not support symlinks:',
)
print('\tgit config core.symlinks false')
return 1
else:
return 0
if __name__ == '__main__':
raise SystemExit(main())

View file

@ -1,18 +1,18 @@
from __future__ import annotations from __future__ import print_function
from __future__ import unicode_literals
import argparse import argparse
import configparser
import os import os
from collections.abc import Sequence from typing import Dict
from typing import NamedTuple from typing import List
from typing import Optional
from typing import Sequence
from typing import Set
from six.moves import configparser
class BadFile(NamedTuple): def get_aws_cred_files_from_env(): # type: () -> Set[str]
filename: str
key: str
def get_aws_cred_files_from_env() -> set[str]:
"""Extract credential file paths from environment variables.""" """Extract credential file paths from environment variables."""
return { return {
os.environ[env_var] os.environ[env_var]
@ -24,18 +24,18 @@ def get_aws_cred_files_from_env() -> set[str]:
} }
def get_aws_secrets_from_env() -> set[str]: def get_aws_secrets_from_env(): # type: () -> Set[str]
"""Extract AWS secrets from environment variables.""" """Extract AWS secrets from environment variables."""
keys = set() keys = set()
for env_var in ( for env_var in (
'AWS_SECRET_ACCESS_KEY', 'AWS_SECURITY_TOKEN', 'AWS_SESSION_TOKEN', 'AWS_SECRET_ACCESS_KEY', 'AWS_SECURITY_TOKEN', 'AWS_SESSION_TOKEN',
): ):
if os.environ.get(env_var): if env_var in os.environ:
keys.add(os.environ[env_var]) keys.add(os.environ[env_var])
return keys return keys
def get_aws_secrets_from_file(credentials_file: str) -> set[str]: def get_aws_secrets_from_file(credentials_file): # type: (str) -> Set[str]
"""Extract AWS secrets from configuration files. """Extract AWS secrets from configuration files.
Read an ini-style configuration file and return a set with all found AWS Read an ini-style configuration file and return a set with all found AWS
@ -66,10 +66,8 @@ def get_aws_secrets_from_file(credentials_file: str) -> set[str]:
return keys return keys
def check_file_for_aws_keys( def check_file_for_aws_keys(filenames, keys):
filenames: Sequence[str], # type: (Sequence[str], Set[str]) -> List[Dict[str, str]]
keys: set[bytes],
) -> list[BadFile]:
"""Check if files contain AWS secrets. """Check if files contain AWS secrets.
Return a list of all files containing AWS secrets and keys found, with all Return a list of all files containing AWS secrets and keys found, with all
@ -78,18 +76,19 @@ def check_file_for_aws_keys(
bad_files = [] bad_files = []
for filename in filenames: for filename in filenames:
with open(filename, 'rb') as content: with open(filename, 'r') as content:
text_body = content.read() text_body = content.read()
for key in keys: for key in keys:
# naively match the entire file, low chance of incorrect # naively match the entire file, low chance of incorrect
# collision # collision
if key in text_body: if key in text_body:
key_hidden = key.decode()[:4].ljust(28, '*') bad_files.append({
bad_files.append(BadFile(filename, key_hidden)) 'filename': filename, 'key': key[:4] + '*' * 28,
})
return bad_files return bad_files
def main(argv: Sequence[str] | None = None) -> int: def main(argv=None): # type: (Optional[Sequence[str]]) -> int
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument('filenames', nargs='+', help='Filenames to run') parser.add_argument('filenames', nargs='+', help='Filenames to run')
parser.add_argument( parser.add_argument(
@ -118,7 +117,7 @@ def main(argv: Sequence[str] | None = None) -> int:
# of files to to gather AWS secrets from. # of files to to gather AWS secrets from.
credential_files |= get_aws_cred_files_from_env() credential_files |= get_aws_cred_files_from_env()
keys: set[str] = set() keys = set() # type: Set[str]
for credential_file in credential_files: for credential_file in credential_files:
keys |= get_aws_secrets_from_file(credential_file) keys |= get_aws_secrets_from_file(credential_file)
@ -137,15 +136,14 @@ def main(argv: Sequence[str] | None = None) -> int:
) )
return 2 return 2
keys_b = {key.encode() for key in keys} bad_filenames = check_file_for_aws_keys(args.filenames, keys)
bad_filenames = check_file_for_aws_keys(args.filenames, keys_b)
if bad_filenames: if bad_filenames:
for bad_file in bad_filenames: for bad_file in bad_filenames:
print(f'AWS secret found in {bad_file.filename}: {bad_file.key}') print('AWS secret found in {filename}: {key}'.format(**bad_file))
return 1 return 1
else: else:
return 0 return 0
if __name__ == '__main__': if __name__ == '__main__':
raise SystemExit(main()) exit(main())

View file

@ -1,7 +1,9 @@
from __future__ import annotations from __future__ import print_function
import argparse import argparse
from collections.abc import Sequence import sys
from typing import Optional
from typing import Sequence
BLACKLIST = [ BLACKLIST = [
b'BEGIN RSA PRIVATE KEY', b'BEGIN RSA PRIVATE KEY',
@ -12,12 +14,10 @@ BLACKLIST = [
b'PuTTY-User-Key-File-2', b'PuTTY-User-Key-File-2',
b'BEGIN SSH2 ENCRYPTED PRIVATE KEY', b'BEGIN SSH2 ENCRYPTED PRIVATE KEY',
b'BEGIN PGP PRIVATE KEY BLOCK', b'BEGIN PGP PRIVATE KEY BLOCK',
b'BEGIN ENCRYPTED PRIVATE KEY',
b'BEGIN OpenVPN Static key V1',
] ]
def main(argv: Sequence[str] | None = None) -> int: def main(argv=None): # type: (Optional[Sequence[str]]) -> int
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument('filenames', nargs='*', help='Filenames to check') parser.add_argument('filenames', nargs='*', help='Filenames to check')
args = parser.parse_args(argv) args = parser.parse_args(argv)
@ -32,11 +32,11 @@ def main(argv: Sequence[str] | None = None) -> int:
if private_key_files: if private_key_files:
for private_key_file in private_key_files: for private_key_file in private_key_files:
print(f'Private key found: {private_key_file}') print('Private key found: {}'.format(private_key_file))
return 1 return 1
else: else:
return 0 return 0
if __name__ == '__main__': if __name__ == '__main__':
raise SystemExit(main()) sys.exit(main())

View file

@ -1,17 +1,20 @@
from __future__ import annotations from __future__ import print_function
from __future__ import unicode_literals
import argparse import argparse
import os import os
from collections.abc import Sequence import sys
from typing import IO from typing import IO
from typing import Optional
from typing import Sequence
def fix_file(file_obj: IO[bytes]) -> int: def fix_file(file_obj): # type: (IO[bytes]) -> int
# Test for newline at end of file # Test for newline at end of file
# Empty files will throw IOError here # Empty files will throw IOError here
try: try:
file_obj.seek(-1, os.SEEK_END) file_obj.seek(-1, os.SEEK_END)
except OSError: except IOError:
return 0 return 0
last_character = file_obj.read(1) last_character = file_obj.read(1)
# last_character will be '' for an empty file # last_character will be '' for an empty file
@ -49,7 +52,7 @@ def fix_file(file_obj: IO[bytes]) -> int:
return 0 return 0
def main(argv: Sequence[str] | None = None) -> int: def main(argv=None): # type: (Optional[Sequence[str]]) -> int
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument('filenames', nargs='*', help='Filenames to fix') parser.add_argument('filenames', nargs='*', help='Filenames to fix')
args = parser.parse_args(argv) args = parser.parse_args(argv)
@ -61,11 +64,11 @@ def main(argv: Sequence[str] | None = None) -> int:
with open(filename, 'rb+') as file_obj: with open(filename, 'rb+') as file_obj:
ret_for_file = fix_file(file_obj) ret_for_file = fix_file(file_obj)
if ret_for_file: if ret_for_file:
print(f'Fixing {filename}') print('Fixing {}'.format(filename))
retv |= ret_for_file retv |= ret_for_file
return retv return retv
if __name__ == '__main__': if __name__ == '__main__':
raise SystemExit(main()) sys.exit(main())

View file

@ -2,45 +2,31 @@
A very simple pre-commit hook that, when passed one or more filenames A very simple pre-commit hook that, when passed one or more filenames
as arguments, will sort the lines in those files. as arguments, will sort the lines in those files.
An example use case for this: you have a deploy-allowlist.txt file An example use case for this: you have a deploy-whitelist.txt file
in a repo that contains a list of filenames that is used to specify in a repo that contains a list of filenames that is used to specify
files to be included in a docker container. This file has one filename files to be included in a docker container. This file has one filename
per line. Various users are adding/removing lines from this file; using per line. Various users are adding/removing lines from this file; using
this hook on that file should reduce the instances of git merge this hook on that file should reduce the instances of git merge
conflicts and keep the file nicely ordered. conflicts and keep the file nicely ordered.
""" """
from __future__ import annotations from __future__ import print_function
import argparse import argparse
from collections.abc import Callable import sys
from collections.abc import Iterable
from collections.abc import Sequence
from typing import Any
from typing import IO from typing import IO
from typing import Optional
from typing import Sequence
PASS = 0 PASS = 0
FAIL = 1 FAIL = 1
def sort_file_contents( def sort_file_contents(f): # type: (IO[bytes]) -> int
f: IO[bytes],
key: Callable[[bytes], Any] | None,
*,
unique: bool = False,
) -> int:
before = list(f) before = list(f)
lines: Iterable[bytes] = ( after = sorted([line.strip(b'\n\r') for line in before if line.strip()])
line.rstrip(b'\n\r') for line in before if line.strip()
)
if unique:
lines = set(lines)
after = sorted(lines, key=key)
before_string = b''.join(before) before_string = b''.join(before)
after_string = b'\n'.join(after) after_string = b'\n'.join(after) + b'\n'
if after_string:
after_string += b'\n'
if before_string == after_string: if before_string == after_string:
return PASS return PASS
@ -51,36 +37,19 @@ def sort_file_contents(
return FAIL return FAIL
def main(argv: Sequence[str] | None = None) -> int: def main(argv=None): # type: (Optional[Sequence[str]]) -> int
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument('filenames', nargs='+', help='Files to sort') parser.add_argument('filenames', nargs='+', help='Files to sort')
mutex = parser.add_mutually_exclusive_group(required=False)
mutex.add_argument(
'--ignore-case',
action='store_const',
const=bytes.lower,
default=None,
help='fold lower case to upper case characters',
)
mutex.add_argument(
'--unique',
action='store_true',
help='ensure each line is unique',
)
args = parser.parse_args(argv) args = parser.parse_args(argv)
retv = PASS retv = PASS
for arg in args.filenames: for arg in args.filenames:
with open(arg, 'rb+') as file_obj: with open(arg, 'rb+') as file_obj:
ret_for_file = sort_file_contents( ret_for_file = sort_file_contents(file_obj)
file_obj, key=args.ignore_case, unique=args.unique,
)
if ret_for_file: if ret_for_file:
print(f'Sorting {arg}') print('Sorting {}'.format(arg))
retv |= ret_for_file retv |= ret_for_file
@ -88,4 +57,4 @@ def main(argv: Sequence[str] | None = None) -> int:
if __name__ == '__main__': if __name__ == '__main__':
raise SystemExit(main()) sys.exit(main())

View file

@ -1,31 +0,0 @@
from __future__ import annotations
import argparse
from collections.abc import Sequence
def main(argv: Sequence[str] | None = None) -> int:
parser = argparse.ArgumentParser()
parser.add_argument('filenames', nargs='*', help='Filenames to check')
args = parser.parse_args(argv)
retv = 0
for filename in args.filenames:
with open(filename, 'rb') as f_b:
bts = f_b.read(3)
if bts == b'\xef\xbb\xbf':
with open(filename, newline='', encoding='utf-8-sig') as f:
contents = f.read()
with open(filename, 'w', newline='', encoding='utf-8') as f:
f.write(contents)
print(f'{filename}: removed byte-order marker')
retv = 1
return retv
if __name__ == '__main__':
raise SystemExit(main())

View file

@ -0,0 +1,153 @@
from __future__ import absolute_import
from __future__ import print_function
from __future__ import unicode_literals
import argparse
import collections
from typing import IO
from typing import Optional
from typing import Sequence
from typing import Union
DEFAULT_PRAGMA = b'# -*- coding: utf-8 -*-'
def has_coding(line): # type: (bytes) -> bool
if not line.strip():
return False
return (
line.lstrip()[:1] == b'#' and (
b'unicode' in line or
b'encoding' in line or
b'coding:' in line or
b'coding=' in line
)
)
class ExpectedContents(
collections.namedtuple(
'ExpectedContents', ('shebang', 'rest', 'pragma_status', 'ending'),
),
):
"""
pragma_status:
- True: has exactly the coding pragma expected
- False: missing coding pragma entirely
- None: has a coding pragma, but it does not match
"""
__slots__ = ()
@property
def has_any_pragma(self): # type: () -> bool
return self.pragma_status is not False
def is_expected_pragma(self, remove): # type: (bool) -> bool
expected_pragma_status = not remove
return self.pragma_status is expected_pragma_status
def _get_expected_contents(first_line, second_line, rest, expected_pragma):
# type: (bytes, bytes, bytes, bytes) -> ExpectedContents
ending = b'\r\n' if first_line.endswith(b'\r\n') else b'\n'
if first_line.startswith(b'#!'):
shebang = first_line
potential_coding = second_line
else:
shebang = b''
potential_coding = first_line
rest = second_line + rest
if potential_coding.rstrip(b'\r\n') == expected_pragma:
pragma_status = True # type: Optional[bool]
elif has_coding(potential_coding):
pragma_status = None
else:
pragma_status = False
rest = potential_coding + rest
return ExpectedContents(
shebang=shebang, rest=rest, pragma_status=pragma_status, ending=ending,
)
def fix_encoding_pragma(f, remove=False, expected_pragma=DEFAULT_PRAGMA):
# type: (IO[bytes], bool, bytes) -> int
expected = _get_expected_contents(
f.readline(), f.readline(), f.read(), expected_pragma,
)
# Special cases for empty files
if not expected.rest.strip():
# If a file only has a shebang or a coding pragma, remove it
if expected.has_any_pragma or expected.shebang:
f.seek(0)
f.truncate()
f.write(b'')
return 1
else:
return 0
if expected.is_expected_pragma(remove):
return 0
# Otherwise, write out the new file
f.seek(0)
f.truncate()
f.write(expected.shebang)
if not remove:
f.write(expected_pragma + expected.ending)
f.write(expected.rest)
return 1
def _normalize_pragma(pragma): # type: (Union[bytes, str]) -> bytes
if not isinstance(pragma, bytes):
pragma = pragma.encode('UTF-8')
return pragma.rstrip()
def main(argv=None): # type: (Optional[Sequence[str]]) -> int
parser = argparse.ArgumentParser(
'Fixes the encoding pragma of python files',
)
parser.add_argument('filenames', nargs='*', help='Filenames to fix')
parser.add_argument(
'--pragma', default=DEFAULT_PRAGMA, type=_normalize_pragma,
help='The encoding pragma to use. Default: {}'.format(
DEFAULT_PRAGMA.decode(),
),
)
parser.add_argument(
'--remove', action='store_true',
help='Remove the encoding pragma (Useful in a python3-only codebase)',
)
args = parser.parse_args(argv)
retv = 0
if args.remove:
fmt = 'Removed encoding pragma from {filename}'
else:
fmt = 'Added `{pragma}` to {filename}'
for filename in args.filenames:
with open(filename, 'r+b') as f:
file_ret = fix_encoding_pragma(
f, remove=args.remove, expected_pragma=args.pragma,
)
retv |= file_ret
if file_ret:
print(
fmt.format(
pragma=args.pragma.decode(), filename=filename,
),
)
return retv
if __name__ == '__main__':
exit(main())

View file

@ -1,37 +1,25 @@
from __future__ import annotations from __future__ import absolute_import
from __future__ import print_function
from __future__ import unicode_literals
import argparse from typing import Optional
import os from typing import Sequence
from collections.abc import Sequence
from pre_commit_hooks.util import cmd_output from pre_commit_hooks.util import cmd_output
def main(argv: Sequence[str] | None = None) -> int: def main(argv=None): # type: (Optional[Sequence[str]]) -> int
parser = argparse.ArgumentParser() # `argv` is ignored, pre-commit will send us a list of files that we
parser.add_argument('filenames', nargs='*') # don't care about
args = parser.parse_args(argv)
if (
'PRE_COMMIT_FROM_REF' in os.environ and
'PRE_COMMIT_TO_REF' in os.environ
):
diff_arg = '...'.join((
os.environ['PRE_COMMIT_FROM_REF'],
os.environ['PRE_COMMIT_TO_REF'],
))
else:
diff_arg = '--staged'
added_diff = cmd_output( added_diff = cmd_output(
'git', 'diff', '--diff-filter=A', '--raw', diff_arg, '--', 'git', 'diff', '--staged', '--diff-filter=A', '--raw',
*args.filenames,
) )
retv = 0 retv = 0
for line in added_diff.splitlines(): for line in added_diff.splitlines():
metadata, filename = line.split('\t', 1) metadata, filename = line.split('\t', 1)
new_mode = metadata.split(' ')[1] new_mode = metadata.split(' ')[1]
if new_mode == '160000': if new_mode == '160000':
print(f'{filename}: new submodule introduced') print('{}: new submodule introduced'.format(filename))
retv = 1 retv = 1
if retv: if retv:
@ -45,4 +33,4 @@ def main(argv: Sequence[str] | None = None) -> int:
if __name__ == '__main__': if __name__ == '__main__':
raise SystemExit(main()) exit(main())

View file

@ -1,8 +1,12 @@
from __future__ import annotations from __future__ import absolute_import
from __future__ import print_function
from __future__ import unicode_literals
import argparse import argparse
import collections import collections
from collections.abc import Sequence from typing import Dict
from typing import Optional
from typing import Sequence
CRLF = b'\r\n' CRLF = b'\r\n'
@ -13,7 +17,7 @@ ALL_ENDINGS = (CR, CRLF, LF)
FIX_TO_LINE_ENDING = {'cr': CR, 'crlf': CRLF, 'lf': LF} FIX_TO_LINE_ENDING = {'cr': CR, 'crlf': CRLF, 'lf': LF}
def _fix(filename: str, contents: bytes, ending: bytes) -> None: def _fix(filename, contents, ending): # type: (str, bytes, bytes) -> None
new_contents = b''.join( new_contents = b''.join(
line.rstrip(b'\r\n') + ending for line in contents.splitlines(True) line.rstrip(b'\r\n') + ending for line in contents.splitlines(True)
) )
@ -21,11 +25,11 @@ def _fix(filename: str, contents: bytes, ending: bytes) -> None:
f.write(new_contents) f.write(new_contents)
def fix_filename(filename: str, fix: str) -> int: def fix_filename(filename, fix): # type: (str, str) -> int
with open(filename, 'rb') as f: with open(filename, 'rb') as f:
contents = f.read() contents = f.read()
counts: dict[bytes, int] = collections.defaultdict(int) counts = collections.defaultdict(int) # type: Dict[bytes, int]
for line in contents.splitlines(True): for line in contents.splitlines(True):
for ending in ALL_ENDINGS: for ending in ALL_ENDINGS:
@ -62,7 +66,7 @@ def fix_filename(filename: str, fix: str) -> int:
return other_endings return other_endings
def main(argv: Sequence[str] | None = None) -> int: def main(argv=None): # type: (Optional[Sequence[str]]) -> int
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument( parser.add_argument(
'-f', '--fix', '-f', '--fix',
@ -77,12 +81,12 @@ def main(argv: Sequence[str] | None = None) -> int:
for filename in args.filenames: for filename in args.filenames:
if fix_filename(filename, args.fix): if fix_filename(filename, args.fix):
if args.fix == 'no': if args.fix == 'no':
print(f'{filename}: mixed line endings') print('{}: mixed line endings'.format(filename))
else: else:
print(f'{filename}: fixed mixed line endings') print('{}: fixed mixed line endings'.format(filename))
retv = 1 retv = 1
return retv return retv
if __name__ == '__main__': if __name__ == '__main__':
raise SystemExit(main()) exit(main())

View file

@ -1,18 +1,17 @@
from __future__ import annotations from __future__ import print_function
import argparse import argparse
import re import re
from collections.abc import Sequence
from typing import AbstractSet from typing import AbstractSet
from typing import Optional
from typing import Sequence
from pre_commit_hooks.util import CalledProcessError from pre_commit_hooks.util import CalledProcessError
from pre_commit_hooks.util import cmd_output from pre_commit_hooks.util import cmd_output
def is_on_branch( def is_on_branch(protected, patterns=frozenset()):
protected: AbstractSet[str], # type: (AbstractSet[str], AbstractSet[str]) -> bool
patterns: AbstractSet[str] = frozenset(),
) -> bool:
try: try:
ref_name = cmd_output('git', 'symbolic-ref', 'HEAD') ref_name = cmd_output('git', 'symbolic-ref', 'HEAD')
except CalledProcessError: except CalledProcessError:
@ -24,7 +23,7 @@ def is_on_branch(
) )
def main(argv: Sequence[str] | None = None) -> int: def main(argv=None): # type: (Optional[Sequence[str]]) -> int
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument( parser.add_argument(
'-b', '--branch', action='append', '-b', '--branch', action='append',
@ -39,10 +38,10 @@ def main(argv: Sequence[str] | None = None) -> int:
) )
args = parser.parse_args(argv) args = parser.parse_args(argv)
protected = frozenset(args.branch or ('master', 'main')) protected = frozenset(args.branch or ('master',))
patterns = frozenset(args.pattern or ()) patterns = frozenset(args.pattern or ())
return int(is_on_branch(protected, patterns)) return int(is_on_branch(protected, patterns))
if __name__ == '__main__': if __name__ == '__main__':
raise SystemExit(main()) exit(main())

View file

@ -1,42 +1,50 @@
from __future__ import annotations from __future__ import print_function
import argparse import argparse
import io
import json import json
import sys import sys
from collections.abc import Mapping from collections import OrderedDict
from collections.abc import Sequence
from difflib import unified_diff from difflib import unified_diff
from typing import List
from typing import Mapping
from typing import Optional
from typing import Sequence
from typing import Tuple
from typing import Union
from six import text_type
def _get_pretty_format( def _get_pretty_format(
contents: str, contents, indent, ensure_ascii=True, sort_keys=True, top_keys=(),
indent: str, ): # type: (str, str, bool, bool, Sequence[str]) -> str
ensure_ascii: bool = True, def pairs_first(pairs):
sort_keys: bool = True, # type: (Sequence[Tuple[str, str]]) -> Mapping[str, str]
top_keys: Sequence[str] = (),
) -> str:
def pairs_first(pairs: Sequence[tuple[str, str]]) -> Mapping[str, str]:
before = [pair for pair in pairs if pair[0] in top_keys] before = [pair for pair in pairs if pair[0] in top_keys]
before = sorted(before, key=lambda x: top_keys.index(x[0])) before = sorted(before, key=lambda x: top_keys.index(x[0]))
after = [pair for pair in pairs if pair[0] not in top_keys] after = [pair for pair in pairs if pair[0] not in top_keys]
if sort_keys: if sort_keys:
after.sort() after = sorted(after, key=lambda x: x[0])
return dict(before + after) return OrderedDict(before + after)
json_pretty = json.dumps( json_pretty = json.dumps(
json.loads(contents, object_pairs_hook=pairs_first), json.loads(contents, object_pairs_hook=pairs_first),
indent=indent, indent=indent,
ensure_ascii=ensure_ascii, ensure_ascii=ensure_ascii,
# Workaround for https://bugs.python.org/issue16333
separators=(',', ': '),
) )
return f'{json_pretty}\n' # Ensure unicode (Py2) and add the newline that dumps does not end with.
return text_type(json_pretty) + '\n'
def _autofix(filename: str, new_contents: str) -> None: def _autofix(filename, new_contents): # type: (str, str) -> None
print(f'Fixing file {filename}') print('Fixing file {}'.format(filename))
with open(filename, 'w', encoding='UTF-8') as f: with io.open(filename, 'w', encoding='UTF-8') as f:
f.write(new_contents) f.write(new_contents)
def parse_num_to_int(s: str) -> int | str: def parse_num_to_int(s): # type: (str) -> Union[int, str]
"""Convert string numbers to int, leaving strings as is.""" """Convert string numbers to int, leaving strings as is."""
try: try:
return int(s) return int(s)
@ -44,18 +52,18 @@ def parse_num_to_int(s: str) -> int | str:
return s return s
def parse_topkeys(s: str) -> list[str]: def parse_topkeys(s): # type: (str) -> List[str]
return s.split(',') return s.split(',')
def get_diff(source: str, target: str, file: str) -> str: def get_diff(source, target, file): # type: (str, str, str) -> str
source_lines = source.splitlines(True) source_lines = source.splitlines(True)
target_lines = target.splitlines(True) target_lines = target.splitlines(True)
diff = unified_diff(source_lines, target_lines, fromfile=file, tofile=file) diff = unified_diff(source_lines, target_lines, fromfile=file, tofile=file)
return ''.join(diff) return ''.join(diff)
def main(argv: Sequence[str] | None = None) -> int: def main(argv=None): # type: (Optional[Sequence[str]]) -> int
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument( parser.add_argument(
'--autofix', '--autofix',
@ -102,7 +110,7 @@ def main(argv: Sequence[str] | None = None) -> int:
status = 0 status = 0
for json_file in args.filenames: for json_file in args.filenames:
with open(json_file, encoding='UTF-8') as f: with io.open(json_file, encoding='UTF-8') as f:
contents = f.read() contents = f.read()
try: try:
@ -110,28 +118,26 @@ def main(argv: Sequence[str] | None = None) -> int:
contents, args.indent, ensure_ascii=not args.no_ensure_ascii, contents, args.indent, ensure_ascii=not args.no_ensure_ascii,
sort_keys=not args.no_sort_keys, top_keys=args.top_keys, sort_keys=not args.no_sort_keys, top_keys=args.top_keys,
) )
except ValueError:
print(
f'Input File {json_file} is not a valid JSON, consider using '
f'check-json',
)
status = 1
else:
if contents != pretty_contents: if contents != pretty_contents:
if args.autofix: if args.autofix:
_autofix(json_file, pretty_contents) _autofix(json_file, pretty_contents)
else: else:
diff_output = get_diff( print(
contents, get_diff(contents, pretty_contents, json_file),
pretty_contents, end='',
json_file,
) )
sys.stdout.buffer.write(diff_output.encode())
status = 1 status = 1
except ValueError:
print(
'Input File {} is not a valid JSON, consider using check-json'
.format(json_file),
)
return 1
return status return status
if __name__ == '__main__': if __name__ == '__main__':
raise SystemExit(main()) sys.exit(main())

View file

@ -1,16 +0,0 @@
from __future__ import annotations
import sys
from collections.abc import Sequence
def main(argv: Sequence[str] | None = None) -> int:
argv = argv if argv is not None else sys.argv[1:]
hookid, new_hookid, url = argv[:3]
raise SystemExit(
f'`{hookid}` has been removed -- use `{new_hookid}` from {url}',
)
if __name__ == '__main__':
raise SystemExit(main())

View file

@ -1,42 +1,33 @@
from __future__ import annotations from __future__ import print_function
import argparse import argparse
import re
from collections.abc import Sequence
from typing import IO from typing import IO
from typing import List
from typing import Optional
from typing import Sequence
PASS = 0 PASS = 0
FAIL = 1 FAIL = 1
class Requirement: class Requirement(object):
UNTIL_COMPARISON = re.compile(b'={2,3}|!=|~=|>=?|<=?')
UNTIL_SEP = re.compile(rb'[^;\s]+')
def __init__(self) -> None: def __init__(self): # type: () -> None
self.value: bytes | None = None super(Requirement, self).__init__()
self.comments: list[bytes] = [] self.value = None # type: Optional[bytes]
self.comments = [] # type: List[bytes]
@property @property
def name(self) -> bytes: def name(self): # type: () -> bytes
assert self.value is not None, self.value assert self.value is not None, self.value
name = self.value.lower()
for egg in (b'#egg=', b'&egg='): for egg in (b'#egg=', b'&egg='):
if egg in self.value: if egg in self.value:
return name.partition(egg)[-1] return self.value.lower().partition(egg)[-1]
m = self.UNTIL_SEP.match(name) return self.value.lower().partition(b'==')[0]
assert m is not None
name = m.group() def __lt__(self, requirement): # type: (Requirement) -> int
m = self.UNTIL_COMPARISON.search(name)
if not m:
return name
return name[:m.start()]
def __lt__(self, requirement: Requirement) -> bool:
# \n means top of file comment, so always return True, # \n means top of file comment, so always return True,
# otherwise just do a string comparison with value. # otherwise just do a string comparison with value.
assert self.value is not None, self.value assert self.value is not None, self.value
@ -45,30 +36,13 @@ class Requirement:
elif requirement.value == b'\n': elif requirement.value == b'\n':
return False return False
else: else:
# if 2 requirements have the same name, the one with comments
# needs to go first (so that when removing duplicates, the one
# with comments is kept)
if self.name == requirement.name:
return bool(self.comments) > bool(requirement.comments)
return self.name < requirement.name return self.name < requirement.name
def is_complete(self) -> bool:
return (
self.value is not None and
not self.value.rstrip(b'\r\n').endswith(b'\\')
)
def append_value(self, value: bytes) -> None: def fix_requirements(f): # type: (IO[bytes]) -> int
if self.value is not None: requirements = [] # type: List[Requirement]
self.value += value
else:
self.value = value
def fix_requirements(f: IO[bytes]) -> int:
requirements: list[Requirement] = []
before = list(f) before = list(f)
after: list[bytes] = [] after = [] # type: List[bytes]
before_string = b''.join(before) before_string = b''.join(before)
@ -85,7 +59,7 @@ def fix_requirements(f: IO[bytes]) -> int:
# If the most recent requirement object has a value, then it's # If the most recent requirement object has a value, then it's
# time to start building the next requirement object. # time to start building the next requirement object.
if not len(requirements) or requirements[-1].is_complete(): if not len(requirements) or requirements[-1].value is not None:
requirements.append(Requirement()) requirements.append(Requirement())
requirement = requirements[-1] requirement = requirements[-1]
@ -100,10 +74,10 @@ def fix_requirements(f: IO[bytes]) -> int:
requirement.value = b'\n' requirement.value = b'\n'
else: else:
requirement.comments.append(line) requirement.comments.append(line)
elif line.lstrip().startswith(b'#') or line.strip() == b'': elif line.startswith(b'#') or line.strip() == b'':
requirement.comments.append(line) requirement.comments.append(line)
else: else:
requirement.append_value(line) requirement.value = line
# if a file ends in a comment, preserve it at the end # if a file ends in a comment, preserve it at the end
if requirements[-1].value is None: if requirements[-1].value is None:
@ -115,20 +89,13 @@ def fix_requirements(f: IO[bytes]) -> int:
# which is automatically added by broken pip package under Debian # which is automatically added by broken pip package under Debian
requirements = [ requirements = [
req for req in requirements req for req in requirements
if req.value not in [ if req.value != b'pkg-resources==0.0.0\n'
b'pkg-resources==0.0.0\n',
b'pkg_resources==0.0.0\n',
]
] ]
# sort the requirements and remove duplicates
prev = None
for requirement in sorted(requirements): for requirement in sorted(requirements):
after.extend(requirement.comments) after.extend(requirement.comments)
assert requirement.value, requirement.value assert requirement.value, requirement.value
if prev is None or requirement.value != prev.value:
after.append(requirement.value) after.append(requirement.value)
prev = requirement
after.extend(rest) after.extend(rest)
after_string = b''.join(after) after_string = b''.join(after)
@ -142,7 +109,7 @@ def fix_requirements(f: IO[bytes]) -> int:
return FAIL return FAIL
def main(argv: Sequence[str] | None = None) -> int: def main(argv=None): # type: (Optional[Sequence[str]]) -> int
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument('filenames', nargs='*', help='Filenames to fix') parser.add_argument('filenames', nargs='*', help='Filenames to fix')
args = parser.parse_args(argv) args = parser.parse_args(argv)
@ -154,7 +121,7 @@ def main(argv: Sequence[str] | None = None) -> int:
ret_for_file = fix_requirements(file_obj) ret_for_file = fix_requirements(file_obj)
if ret_for_file: if ret_for_file:
print(f'Sorting {arg}') print('Sorting {}'.format(arg))
retv |= ret_for_file retv |= ret_for_file
@ -162,4 +129,4 @@ def main(argv: Sequence[str] | None = None) -> int:
if __name__ == '__main__': if __name__ == '__main__':
raise SystemExit(main()) exit(main())

21
pre_commit_hooks/sort_simple_yaml.py Normal file → Executable file
View file

@ -1,3 +1,4 @@
#!/usr/bin/env python
"""Sort a simple YAML file, keeping blocks of comments and definitions """Sort a simple YAML file, keeping blocks of comments and definitions
together. together.
@ -17,16 +18,18 @@ We assume a strict subset of YAML that looks like:
In other words, we don't sort deeper than the top layer, and might corrupt In other words, we don't sort deeper than the top layer, and might corrupt
complicated YAML files. complicated YAML files.
""" """
from __future__ import annotations from __future__ import print_function
import argparse import argparse
from collections.abc import Sequence from typing import List
from typing import Optional
from typing import Sequence
QUOTES = ["'", '"'] QUOTES = ["'", '"']
def sort(lines: list[str]) -> list[str]: def sort(lines): # type: (List[str]) -> List[str]
"""Sort a YAML file in alphabetical order, keeping blocks together. """Sort a YAML file in alphabetical order, keeping blocks together.
:param lines: array of strings (without newlines) :param lines: array of strings (without newlines)
@ -44,7 +47,7 @@ def sort(lines: list[str]) -> list[str]:
return new_lines return new_lines
def parse_block(lines: list[str], header: bool = False) -> list[str]: def parse_block(lines, header=False): # type: (List[str], bool) -> List[str]
"""Parse and return a single block, popping off the start of `lines`. """Parse and return a single block, popping off the start of `lines`.
If parsing a header block, we stop after we reach a line that is not a If parsing a header block, we stop after we reach a line that is not a
@ -60,7 +63,7 @@ def parse_block(lines: list[str], header: bool = False) -> list[str]:
return block_lines return block_lines
def parse_blocks(lines: list[str]) -> list[list[str]]: def parse_blocks(lines): # type: (List[str]) -> List[List[str]]
"""Parse and return all possible blocks, popping off the start of `lines`. """Parse and return all possible blocks, popping off the start of `lines`.
:param lines: list of lines :param lines: list of lines
@ -77,7 +80,7 @@ def parse_blocks(lines: list[str]) -> list[list[str]]:
return blocks return blocks
def first_key(lines: list[str]) -> str: def first_key(lines): # type: (List[str]) -> str
"""Returns a string representing the sort key of a block. """Returns a string representing the sort key of a block.
The sort key is the first YAML key we encounter, ignoring comments, and The sort key is the first YAML key we encounter, ignoring comments, and
@ -99,7 +102,7 @@ def first_key(lines: list[str]) -> str:
return '' # not actually reached in reality return '' # not actually reached in reality
def main(argv: Sequence[str] | None = None) -> int: def main(argv=None): # type: (Optional[Sequence[str]]) -> int
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument('filenames', nargs='*', help='Filenames to fix') parser.add_argument('filenames', nargs='*', help='Filenames to fix')
args = parser.parse_args(argv) args = parser.parse_args(argv)
@ -112,7 +115,7 @@ def main(argv: Sequence[str] | None = None) -> int:
new_lines = sort(lines) new_lines = sort(lines)
if lines != new_lines: if lines != new_lines:
print(f'Fixing file `{filename}`') print('Fixing file `{filename}`'.format(filename=filename))
f.seek(0) f.seek(0)
f.write('\n'.join(new_lines) + '\n') f.write('\n'.join(new_lines) + '\n')
f.truncate() f.truncate()
@ -122,4 +125,4 @@ def main(argv: Sequence[str] | None = None) -> int:
if __name__ == '__main__': if __name__ == '__main__':
raise SystemExit(main()) exit(main())

View file

@ -1,22 +1,19 @@
from __future__ import annotations from __future__ import absolute_import
from __future__ import print_function
from __future__ import unicode_literals
import argparse import argparse
import io import io
import re import re
import sys
import tokenize import tokenize
from collections.abc import Sequence from typing import List
from typing import Optional
if sys.version_info >= (3, 12): # pragma: >=3.12 cover from typing import Sequence
FSTRING_START = tokenize.FSTRING_START
FSTRING_END = tokenize.FSTRING_END
else: # pragma: <3.12 cover
FSTRING_START = FSTRING_END = -1
START_QUOTE_RE = re.compile('^[a-zA-Z]*"') START_QUOTE_RE = re.compile('^[a-zA-Z]*"')
def handle_match(token_text: str) -> str: def handle_match(token_text): # type: (str) -> str
if '"""' in token_text or "'''" in token_text: if '"""' in token_text or "'''" in token_text:
return token_text return token_text
@ -31,7 +28,7 @@ def handle_match(token_text: str) -> str:
return token_text return token_text
def get_line_offsets_by_line_no(src: str) -> list[int]: def get_line_offsets_by_line_no(src): # type: (str) -> List[int]
# Padded so we can index with line number # Padded so we can index with line number
offsets = [-1, 0] offsets = [-1, 0]
for line in src.splitlines(True): for line in src.splitlines(True):
@ -39,25 +36,19 @@ def get_line_offsets_by_line_no(src: str) -> list[int]:
return offsets return offsets
def fix_strings(filename: str) -> int: def fix_strings(filename): # type: (str) -> int
with open(filename, encoding='UTF-8', newline='') as f: with io.open(filename, encoding='UTF-8', newline='') as f:
contents = f.read() contents = f.read()
line_offsets = get_line_offsets_by_line_no(contents) line_offsets = get_line_offsets_by_line_no(contents)
# Basically a mutable string # Basically a mutable string
splitcontents = list(contents) splitcontents = list(contents)
fstring_depth = 0
# Iterate in reverse so the offsets are always correct # Iterate in reverse so the offsets are always correct
tokens_l = list(tokenize.generate_tokens(io.StringIO(contents).readline)) tokens_l = list(tokenize.generate_tokens(io.StringIO(contents).readline))
tokens = reversed(tokens_l) tokens = reversed(tokens_l)
for token_type, token_text, (srow, scol), (erow, ecol), _ in tokens: for token_type, token_text, (srow, scol), (erow, ecol), _ in tokens:
if token_type == FSTRING_START: # pragma: >=3.12 cover if token_type == tokenize.STRING:
fstring_depth += 1
elif token_type == FSTRING_END: # pragma: >=3.12 cover
fstring_depth -= 1
elif fstring_depth == 0 and token_type == tokenize.STRING:
new_text = handle_match(token_text) new_text = handle_match(token_text)
splitcontents[ splitcontents[
line_offsets[srow] + scol: line_offsets[srow] + scol:
@ -66,14 +57,14 @@ def fix_strings(filename: str) -> int:
new_contents = ''.join(splitcontents) new_contents = ''.join(splitcontents)
if contents != new_contents: if contents != new_contents:
with open(filename, 'w', encoding='UTF-8', newline='') as f: with io.open(filename, 'w', encoding='UTF-8', newline='') as f:
f.write(new_contents) f.write(new_contents)
return 1 return 1
else: else:
return 0 return 0
def main(argv: Sequence[str] | None = None) -> int: def main(argv=None): # type: (Optional[Sequence[str]]) -> int
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument('filenames', nargs='*', help='Filenames to fix') parser.add_argument('filenames', nargs='*', help='Filenames to fix')
args = parser.parse_args(argv) args = parser.parse_args(argv)
@ -83,11 +74,11 @@ def main(argv: Sequence[str] | None = None) -> int:
for filename in args.filenames: for filename in args.filenames:
return_value = fix_strings(filename) return_value = fix_strings(filename)
if return_value != 0: if return_value != 0:
print(f'Fixing strings in {filename}') print('Fixing strings in {}'.format(filename))
retv |= return_value retv |= return_value
return retv return retv
if __name__ == '__main__': if __name__ == '__main__':
raise SystemExit(main()) exit(main())

View file

@ -1,53 +1,40 @@
from __future__ import annotations from __future__ import print_function
import argparse import argparse
import os.path import os.path
import re import re
from collections.abc import Sequence import sys
from typing import Optional
from typing import Sequence
def main(argv: Sequence[str] | None = None) -> int: def main(argv=None): # type: (Optional[Sequence[str]]) -> int
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument('filenames', nargs='*') parser.add_argument('filenames', nargs='*')
mutex = parser.add_mutually_exclusive_group() parser.add_argument(
mutex.add_argument( '--django', default=False, action='store_true',
'--pytest', help='Use Django-style test naming pattern (test*.py)',
dest='pattern',
action='store_const',
const=r'.*_test\.py',
default=r'.*_test\.py',
help='(the default) ensure tests match %(const)s',
)
mutex.add_argument(
'--pytest-test-first',
dest='pattern',
action='store_const',
const=r'test_.*\.py',
help='ensure tests match %(const)s',
)
mutex.add_argument(
'--django', '--unittest',
dest='pattern',
action='store_const',
const=r'test.*\.py',
help='ensure tests match %(const)s',
) )
args = parser.parse_args(argv) args = parser.parse_args(argv)
retcode = 0 retcode = 0
reg = re.compile(args.pattern) test_name_pattern = r'test.*\.py' if args.django else r'.*_test\.py'
for filename in args.filenames: for filename in args.filenames:
base = os.path.basename(filename) base = os.path.basename(filename)
if ( if (
not reg.fullmatch(base) and not re.match(test_name_pattern, base) and
not base == '__init__.py' and not base == '__init__.py' and
not base == 'conftest.py' not base == 'conftest.py'
): ):
retcode = 1 retcode = 1
print(f'{filename} does not match pattern "{args.pattern}"') print(
'{} does not match pattern "{}"'.format(
filename, test_name_pattern,
),
)
return retcode return retcode
if __name__ == '__main__': if __name__ == '__main__':
raise SystemExit(main()) sys.exit(main())

View file

@ -1,15 +1,14 @@
from __future__ import annotations from __future__ import print_function
import argparse import argparse
import os import os
from collections.abc import Sequence import sys
from typing import Optional
from typing import Sequence
def _fix_file( def _fix_file(filename, is_markdown, chars):
filename: str, # type: (str, bool, Optional[bytes]) -> bool
is_markdown: bool,
chars: bytes | None,
) -> bool:
with open(filename, mode='rb') as file_processed: with open(filename, mode='rb') as file_processed:
lines = file_processed.readlines() lines = file_processed.readlines()
newlines = [_process_line(line, is_markdown, chars) for line in lines] newlines = [_process_line(line, is_markdown, chars) for line in lines]
@ -22,11 +21,8 @@ def _fix_file(
return False return False
def _process_line( def _process_line(line, is_markdown, chars):
line: bytes, # type: (bytes, bool, Optional[bytes]) -> bytes
is_markdown: bool,
chars: bytes | None,
) -> bytes:
if line[-2:] == b'\r\n': if line[-2:] == b'\r\n':
eol = b'\r\n' eol = b'\r\n'
line = line[:-2] line = line[:-2]
@ -41,7 +37,7 @@ def _process_line(
return line.rstrip(chars) + eol return line.rstrip(chars) + eol
def main(argv: Sequence[str] | None = None) -> int: def main(argv=None): # type: (Optional[Sequence[str]]) -> int
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument( parser.add_argument(
'--no-markdown-linebreak-ext', '--no-markdown-linebreak-ext',
@ -84,20 +80,20 @@ def main(argv: Sequence[str] | None = None) -> int:
for ext in md_exts: for ext in md_exts:
if any(c in ext[1:] for c in r'./\:'): if any(c in ext[1:] for c in r'./\:'):
parser.error( parser.error(
f'bad --markdown-linebreak-ext extension ' 'bad --markdown-linebreak-ext extension {!r} (has . / \\ :)\n'
f'{ext!r} (has . / \\ :)\n' " (probably filename; use '--markdown-linebreak-ext=EXT')"
f" (probably filename; use '--markdown-linebreak-ext=EXT')", .format(ext),
) )
chars = None if args.chars is None else args.chars.encode() chars = None if args.chars is None else args.chars.encode('utf-8')
return_code = 0 return_code = 0
for filename in args.filenames: for filename in args.filenames:
_, extension = os.path.splitext(filename.lower()) _, extension = os.path.splitext(filename.lower())
md = all_markdown or extension in md_exts md = all_markdown or extension in md_exts
if _fix_file(filename, md, chars): if _fix_file(filename, md, chars):
print(f'Fixing {filename}') print('Fixing {}'.format(filename))
return_code = 1 return_code = 1
return return_code return return_code
if __name__ == '__main__': if __name__ == '__main__':
raise SystemExit(main()) sys.exit(main())

View file

@ -1,32 +1,28 @@
from __future__ import annotations from __future__ import absolute_import
from __future__ import print_function
from __future__ import unicode_literals
import subprocess import subprocess
from typing import Any from typing import Any
from typing import Set
class CalledProcessError(RuntimeError): class CalledProcessError(RuntimeError):
pass pass
def added_files() -> set[str]: def added_files(): # type: () -> Set[str]
cmd = ('git', 'diff', '--staged', '--name-only', '--diff-filter=A') cmd = ('git', 'diff', '--staged', '--name-only', '--diff-filter=A')
return set(cmd_output(*cmd).splitlines()) return set(cmd_output(*cmd).splitlines())
def cmd_output(*cmd: str, retcode: int | None = 0, **kwargs: Any) -> str: def cmd_output(*cmd, **kwargs): # type: (*str, **Any) -> str
retcode = kwargs.pop('retcode', 0)
kwargs.setdefault('stdout', subprocess.PIPE) kwargs.setdefault('stdout', subprocess.PIPE)
kwargs.setdefault('stderr', subprocess.PIPE) kwargs.setdefault('stderr', subprocess.PIPE)
proc = subprocess.Popen(cmd, **kwargs) proc = subprocess.Popen(cmd, **kwargs)
stdout, stderr = proc.communicate() stdout, stderr = proc.communicate()
stdout = stdout.decode() stdout = stdout.decode('UTF-8')
if retcode is not None and proc.returncode != retcode: if retcode is not None and proc.returncode != retcode:
raise CalledProcessError(cmd, retcode, proc.returncode, stdout, stderr) raise CalledProcessError(cmd, retcode, proc.returncode, stdout, stderr)
return stdout return stdout
def zsplit(s: str) -> list[str]:
s = s.strip('\0')
if s:
return s.split('\0')
else:
return []

View file

@ -1,3 +1,6 @@
covdefaults -e .
coverage coverage
mock
pre-commit
pytest pytest

View file

@ -1,6 +1,6 @@
[metadata] [metadata]
name = pre_commit_hooks name = pre_commit_hooks
version = 6.0.0 version = 2.5.0
description = Some out-of-the-box hooks for pre-commit. description = Some out-of-the-box hooks for pre-commit.
long_description = file: README.md long_description = file: README.md
long_description_content_type = text/markdown long_description_content_type = text/markdown
@ -8,72 +8,76 @@ url = https://github.com/pre-commit/pre-commit-hooks
author = Anthony Sottile author = Anthony Sottile
author_email = asottile@umich.edu author_email = asottile@umich.edu
license = MIT license = MIT
license_files = LICENSE license_file = LICENSE
classifiers = classifiers =
License :: OSI Approved :: MIT License
Programming Language :: Python :: 2
Programming Language :: Python :: 2.7
Programming Language :: Python :: 3 Programming Language :: Python :: 3
Programming Language :: Python :: 3 :: Only Programming Language :: Python :: 3.4
Programming Language :: Python :: 3.5
Programming Language :: Python :: 3.6
Programming Language :: Python :: 3.7
Programming Language :: Python :: Implementation :: CPython Programming Language :: Python :: Implementation :: CPython
Programming Language :: Python :: Implementation :: PyPy Programming Language :: Python :: Implementation :: PyPy
[options] [options]
packages = find: packages = find:
install_requires = install_requires =
flake8
ruamel.yaml>=0.15 ruamel.yaml>=0.15
tomli>=1.1.0;python_version<"3.11" toml
python_requires = >=3.10 six
typing; python_version<"3.5"
[options.packages.find] python_requires = >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*
exclude =
tests*
testing*
[options.entry_points] [options.entry_points]
console_scripts = console_scripts =
autopep8-wrapper = pre_commit_hooks.autopep8_wrapper:main
check-added-large-files = pre_commit_hooks.check_added_large_files:main check-added-large-files = pre_commit_hooks.check_added_large_files:main
check-ast = pre_commit_hooks.check_ast:main check-ast = pre_commit_hooks.check_ast:main
check-builtin-literals = pre_commit_hooks.check_builtin_literals:main check-builtin-literals = pre_commit_hooks.check_builtin_literals:main
check-byte-order-marker = pre_commit_hooks.check_byte_order_marker:main
check-case-conflict = pre_commit_hooks.check_case_conflict:main check-case-conflict = pre_commit_hooks.check_case_conflict:main
check-docstring-first = pre_commit_hooks.check_docstring_first:main check-docstring-first = pre_commit_hooks.check_docstring_first:main
check-executables-have-shebangs = pre_commit_hooks.check_executables_have_shebangs:main check-executables-have-shebangs = pre_commit_hooks.check_executables_have_shebangs:main
check-json = pre_commit_hooks.check_json:main check-json = pre_commit_hooks.check_json:main
check-merge-conflict = pre_commit_hooks.check_merge_conflict:main check-merge-conflict = pre_commit_hooks.check_merge_conflict:main
check-shebang-scripts-are-executable = pre_commit_hooks.check_shebang_scripts_are_executable:main
check-symlinks = pre_commit_hooks.check_symlinks:main check-symlinks = pre_commit_hooks.check_symlinks:main
check-toml = pre_commit_hooks.check_toml:main check-toml = pre_commit_hooks.check_toml:main
check-vcs-permalinks = pre_commit_hooks.check_vcs_permalinks:main check-vcs-permalinks = pre_commit_hooks.check_vcs_permalinks:main
check-xml = pre_commit_hooks.check_xml:main check-xml = pre_commit_hooks.check_xml:main
check-yaml = pre_commit_hooks.check_yaml:main check-yaml = pre_commit_hooks.check_yaml:main
debug-statement-hook = pre_commit_hooks.debug_statement_hook:main debug-statement-hook = pre_commit_hooks.debug_statement_hook:main
destroyed-symlinks = pre_commit_hooks.destroyed_symlinks:main
detect-aws-credentials = pre_commit_hooks.detect_aws_credentials:main detect-aws-credentials = pre_commit_hooks.detect_aws_credentials:main
detect-private-key = pre_commit_hooks.detect_private_key:main detect-private-key = pre_commit_hooks.detect_private_key:main
double-quote-string-fixer = pre_commit_hooks.string_fixer:main double-quote-string-fixer = pre_commit_hooks.string_fixer:main
end-of-file-fixer = pre_commit_hooks.end_of_file_fixer:main end-of-file-fixer = pre_commit_hooks.end_of_file_fixer:main
file-contents-sorter = pre_commit_hooks.file_contents_sorter:main file-contents-sorter = pre_commit_hooks.file_contents_sorter:main
fix-byte-order-marker = pre_commit_hooks.fix_byte_order_marker:main fix-encoding-pragma = pre_commit_hooks.fix_encoding_pragma:main
forbid-new-submodules = pre_commit_hooks.forbid_new_submodules:main forbid-new-submodules = pre_commit_hooks.forbid_new_submodules:main
mixed-line-ending = pre_commit_hooks.mixed_line_ending:main mixed-line-ending = pre_commit_hooks.mixed_line_ending:main
name-tests-test = pre_commit_hooks.tests_should_end_in_test:main name-tests-test = pre_commit_hooks.tests_should_end_in_test:main
no-commit-to-branch = pre_commit_hooks.no_commit_to_branch:main no-commit-to-branch = pre_commit_hooks.no_commit_to_branch:main
pre-commit-hooks-removed = pre_commit_hooks.removed:main
pretty-format-json = pre_commit_hooks.pretty_format_json:main pretty-format-json = pre_commit_hooks.pretty_format_json:main
requirements-txt-fixer = pre_commit_hooks.requirements_txt_fixer:main requirements-txt-fixer = pre_commit_hooks.requirements_txt_fixer:main
sort-simple-yaml = pre_commit_hooks.sort_simple_yaml:main sort-simple-yaml = pre_commit_hooks.sort_simple_yaml:main
trailing-whitespace-fixer = pre_commit_hooks.trailing_whitespace_fixer:main trailing-whitespace-fixer = pre_commit_hooks.trailing_whitespace_fixer:main
[options.packages.find]
exclude =
tests*
testing*
[bdist_wheel] [bdist_wheel]
universal = True universal = True
[coverage:run]
plugins = covdefaults
[mypy] [mypy]
check_untyped_defs = true check_untyped_defs = true
disallow_any_generics = true disallow_any_generics = true
disallow_incomplete_defs = true disallow_incomplete_defs = true
disallow_untyped_defs = true disallow_untyped_defs = true
warn_redundant_casts = true no_implicit_optional = true
warn_unused_ignores = true
[mypy-testing.*] [mypy-testing.*]
disallow_untyped_defs = false disallow_untyped_defs = false

View file

@ -1,4 +1,2 @@
from __future__ import annotations
from setuptools import setup from setuptools import setup
setup() setup()

View file

@ -1,4 +0,0 @@
{
"hello": "world",
"hello": "planet"
}

View file

@ -1,7 +1,7 @@
from __future__ import annotations from __future__ import absolute_import
from __future__ import unicode_literals
import os.path import os.path
import subprocess
TESTING_DIR = os.path.abspath(os.path.dirname(__file__)) TESTING_DIR = os.path.abspath(os.path.dirname(__file__))
@ -9,8 +9,3 @@ TESTING_DIR = os.path.abspath(os.path.dirname(__file__))
def get_resource_path(path): def get_resource_path(path):
return os.path.join(TESTING_DIR, 'resources', path) return os.path.join(TESTING_DIR, 'resources', path)
def git_commit(*args, **kwargs):
cmd = ('git', 'commit', '--no-gpg-sign', '--no-verify', '--no-edit', *args)
subprocess.check_call(cmd, **kwargs)

View file

@ -0,0 +1,13 @@
from __future__ import absolute_import
from __future__ import unicode_literals
import pytest
from pre_commit_hooks.autopep8_wrapper import main
def test_invariantly_fails():
with pytest.raises(SystemExit) as excinfo:
main()
msg, = excinfo.value.args
assert 'https://github.com/pre-commit/mirrors-autopep8' in msg

View file

@ -1,13 +1,13 @@
from __future__ import annotations from __future__ import absolute_import
from __future__ import unicode_literals
import shutil import distutils.spawn
import pytest import pytest
from pre_commit_hooks.check_added_large_files import find_large_added_files from pre_commit_hooks.check_added_large_files import find_large_added_files
from pre_commit_hooks.check_added_large_files import main from pre_commit_hooks.check_added_large_files import main
from pre_commit_hooks.util import cmd_output from pre_commit_hooks.util import cmd_output
from testing.util import git_commit
def test_nothing_added(temp_git_dir): def test_nothing_added(temp_git_dir):
@ -43,17 +43,6 @@ def test_add_something_giant(temp_git_dir):
assert find_large_added_files(['f.py'], 10) == 0 assert find_large_added_files(['f.py'], 10) == 0
def test_enforce_all(temp_git_dir):
with temp_git_dir.as_cwd():
temp_git_dir.join('f.py').write('a' * 10000)
# Should fail, when not staged with enforce_all
assert find_large_added_files(['f.py'], 0, enforce_all=True) == 1
# Should pass, when not staged without enforce_all
assert find_large_added_files(['f.py'], 0, enforce_all=False) == 0
def test_added_file_not_in_pre_commits_list(temp_git_dir): def test_added_file_not_in_pre_commits_list(temp_git_dir):
with temp_git_dir.as_cwd(): with temp_git_dir.as_cwd():
temp_git_dir.join('f.py').write("print('hello world')") temp_git_dir.join('f.py').write("print('hello world')")
@ -78,7 +67,7 @@ def test_integration(temp_git_dir):
def has_gitlfs(): def has_gitlfs():
return shutil.which('git-lfs') is not None return distutils.spawn.find_executable('git-lfs') is not None
xfailif_no_gitlfs = pytest.mark.xfail( xfailif_no_gitlfs = pytest.mark.xfail(
@ -87,9 +76,10 @@ xfailif_no_gitlfs = pytest.mark.xfail(
@xfailif_no_gitlfs @xfailif_no_gitlfs
def test_allows_gitlfs(temp_git_dir): # pragma: no cover def test_allows_gitlfs(temp_git_dir, monkeypatch): # pragma: no cover
with temp_git_dir.as_cwd(): with temp_git_dir.as_cwd():
cmd_output('git', 'lfs', 'install', '--local') monkeypatch.setenv(str('HOME'), str(temp_git_dir.strpath))
cmd_output('git', 'lfs', 'install')
temp_git_dir.join('f.py').write('a' * 10000) temp_git_dir.join('f.py').write('a' * 10000)
cmd_output('git', 'lfs', 'track', 'f.py') cmd_output('git', 'lfs', 'track', 'f.py')
cmd_output('git', 'add', '--', '.') cmd_output('git', 'add', '--', '.')
@ -98,37 +88,15 @@ def test_allows_gitlfs(temp_git_dir): # pragma: no cover
@xfailif_no_gitlfs @xfailif_no_gitlfs
def test_moves_with_gitlfs(temp_git_dir): # pragma: no cover def test_moves_with_gitlfs(temp_git_dir, monkeypatch): # pragma: no cover
with temp_git_dir.as_cwd(): with temp_git_dir.as_cwd():
cmd_output('git', 'lfs', 'install', '--local') monkeypatch.setenv(str('HOME'), str(temp_git_dir.strpath))
cmd_output('git', 'lfs', 'install')
cmd_output('git', 'lfs', 'track', 'a.bin', 'b.bin') cmd_output('git', 'lfs', 'track', 'a.bin', 'b.bin')
# First add the file we're going to move # First add the file we're going to move
temp_git_dir.join('a.bin').write('a' * 10000) temp_git_dir.join('a.bin').write('a' * 10000)
cmd_output('git', 'add', '--', '.') cmd_output('git', 'add', '--', '.')
git_commit('-am', 'foo') cmd_output('git', 'commit', '--no-gpg-sign', '-am', 'foo')
# Now move it and make sure the hook still succeeds # Now move it and make sure the hook still succeeds
cmd_output('git', 'mv', 'a.bin', 'b.bin') cmd_output('git', 'mv', 'a.bin', 'b.bin')
assert main(('--maxkb', '9', 'b.bin')) == 0 assert main(('--maxkb', '9', 'b.bin')) == 0
@xfailif_no_gitlfs
def test_enforce_allows_gitlfs(temp_git_dir): # pragma: no cover
with temp_git_dir.as_cwd():
cmd_output('git', 'lfs', 'install', '--local')
temp_git_dir.join('f.py').write('a' * 10000)
cmd_output('git', 'lfs', 'track', 'f.py')
cmd_output('git', 'add', '--', '.')
# With --enforce-all large files on git lfs should succeed
assert main(('--enforce-all', '--maxkb', '9', 'f.py')) == 0
@xfailif_no_gitlfs
def test_enforce_allows_gitlfs_after_commit(temp_git_dir): # pragma: no cover
with temp_git_dir.as_cwd():
cmd_output('git', 'lfs', 'install', '--local')
temp_git_dir.join('f.py').write('a' * 10000)
cmd_output('git', 'lfs', 'track', 'f.py')
cmd_output('git', 'add', '--', '.')
git_commit('-am', 'foo')
# With --enforce-all large files on git lfs should succeed
assert main(('--enforce-all', '--maxkb', '9', 'f.py')) == 0

View file

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import absolute_import
from __future__ import unicode_literals
from pre_commit_hooks.check_ast import main from pre_commit_hooks.check_ast import main
from testing.util import get_resource_path from testing.util import get_resource_path

View file

@ -1,5 +1,3 @@
from __future__ import annotations
import ast import ast
import pytest import pytest
@ -9,7 +7,7 @@ from pre_commit_hooks.check_builtin_literals import main
from pre_commit_hooks.check_builtin_literals import Visitor from pre_commit_hooks.check_builtin_literals import Visitor
BUILTIN_CONSTRUCTORS = '''\ BUILTIN_CONSTRUCTORS = '''\
import builtins from six.moves import builtins
c1 = complex() c1 = complex()
d1 = dict() d1 = dict()
@ -38,6 +36,11 @@ t1 = ()
''' '''
@pytest.fixture
def visitor():
return Visitor()
@pytest.mark.parametrize( @pytest.mark.parametrize(
('expression', 'calls'), ('expression', 'calls'),
[ [
@ -80,8 +83,7 @@ t1 = ()
('builtins.tuple()', []), ('builtins.tuple()', []),
], ],
) )
def test_non_dict_exprs(expression, calls): def test_non_dict_exprs(visitor, expression, calls):
visitor = Visitor(ignore=set())
visitor.visit(ast.parse(expression)) visitor.visit(ast.parse(expression))
assert visitor.builtin_type_calls == calls assert visitor.builtin_type_calls == calls
@ -98,8 +100,7 @@ def test_non_dict_exprs(expression, calls):
('builtins.dict()', []), ('builtins.dict()', []),
], ],
) )
def test_dict_allow_kwargs_exprs(expression, calls): def test_dict_allow_kwargs_exprs(visitor, expression, calls):
visitor = Visitor(ignore=set())
visitor.visit(ast.parse(expression)) visitor.visit(ast.parse(expression))
assert visitor.builtin_type_calls == calls assert visitor.builtin_type_calls == calls
@ -111,18 +112,17 @@ def test_dict_allow_kwargs_exprs(expression, calls):
('dict(a=1, b=2, c=3)', [Call('dict', 1, 0)]), ('dict(a=1, b=2, c=3)', [Call('dict', 1, 0)]),
("dict(**{'a': 1, 'b': 2, 'c': 3})", [Call('dict', 1, 0)]), ("dict(**{'a': 1, 'b': 2, 'c': 3})", [Call('dict', 1, 0)]),
('builtins.dict()', []), ('builtins.dict()', []),
pytest.param('f(dict())', [Call('dict', 1, 2)], id='nested'),
], ],
) )
def test_dict_no_allow_kwargs_exprs(expression, calls): def test_dict_no_allow_kwargs_exprs(expression, calls):
visitor = Visitor(ignore=set(), allow_dict_kwargs=False) visitor = Visitor(allow_dict_kwargs=False)
visitor.visit(ast.parse(expression)) visitor.visit(ast.parse(expression))
assert visitor.builtin_type_calls == calls assert visitor.builtin_type_calls == calls
def test_ignore_constructors(): def test_ignore_constructors():
visitor = Visitor( visitor = Visitor(
ignore={'complex', 'dict', 'float', 'int', 'list', 'str', 'tuple'}, ignore=('complex', 'dict', 'float', 'int', 'list', 'str', 'tuple'),
) )
visitor.visit(ast.parse(BUILTIN_CONSTRUCTORS)) visitor.visit(ast.parse(BUILTIN_CONSTRUCTORS))
assert visitor.builtin_type_calls == [] assert visitor.builtin_type_calls == []
@ -131,19 +131,19 @@ def test_ignore_constructors():
def test_failing_file(tmpdir): def test_failing_file(tmpdir):
f = tmpdir.join('f.py') f = tmpdir.join('f.py')
f.write(BUILTIN_CONSTRUCTORS) f.write(BUILTIN_CONSTRUCTORS)
rc = main([str(f)]) rc = main([f.strpath])
assert rc == 1 assert rc == 1
def test_passing_file(tmpdir): def test_passing_file(tmpdir):
f = tmpdir.join('f.py') f = tmpdir.join('f.py')
f.write(BUILTIN_LITERALS) f.write(BUILTIN_LITERALS)
rc = main([str(f)]) rc = main([f.strpath])
assert rc == 0 assert rc == 0
def test_failing_file_ignore_all(tmpdir): def test_failing_file_ignore_all(tmpdir):
f = tmpdir.join('f.py') f = tmpdir.join('f.py')
f.write(BUILTIN_CONSTRUCTORS) f.write(BUILTIN_CONSTRUCTORS)
rc = main(['--ignore=complex,dict,float,int,list,str,tuple', str(f)]) rc = main(['--ignore=complex,dict,float,int,list,str,tuple', f.strpath])
assert rc == 0 assert rc == 0

View file

@ -0,0 +1,16 @@
from __future__ import absolute_import
from __future__ import unicode_literals
from pre_commit_hooks import check_byte_order_marker
def test_failure(tmpdir):
f = tmpdir.join('f.txt')
f.write_text('ohai', encoding='utf-8-sig')
assert check_byte_order_marker.main((f.strpath,)) == 1
def test_success(tmpdir):
f = tmpdir.join('f.txt')
f.write_text('ohai', encoding='utf-8')
assert check_byte_order_marker.main((f.strpath,)) == 0

View file

@ -1,26 +1,9 @@
from __future__ import annotations from __future__ import absolute_import
from __future__ import unicode_literals
import sys
import pytest
from pre_commit_hooks.check_case_conflict import find_conflicting_filenames from pre_commit_hooks.check_case_conflict import find_conflicting_filenames
from pre_commit_hooks.check_case_conflict import main from pre_commit_hooks.check_case_conflict import main
from pre_commit_hooks.check_case_conflict import parents
from pre_commit_hooks.util import cmd_output from pre_commit_hooks.util import cmd_output
from testing.util import git_commit
skip_win32 = pytest.mark.skipif(
sys.platform == 'win32',
reason='case conflicts between directories and files',
)
def test_parents():
assert set(parents('a')) == set()
assert set(parents('a/b')) == {'a'}
assert set(parents('a/b/c')) == {'a/b', 'a'}
assert set(parents('a/b/c/d')) == {'a/b/c', 'a/b', 'a'}
def test_nothing_added(temp_git_dir): def test_nothing_added(temp_git_dir):
@ -46,36 +29,6 @@ def test_adding_something_with_conflict(temp_git_dir):
assert find_conflicting_filenames(['f.py', 'F.py']) == 1 assert find_conflicting_filenames(['f.py', 'F.py']) == 1
@skip_win32 # pragma: win32 no cover
def test_adding_files_with_conflicting_directories(temp_git_dir):
with temp_git_dir.as_cwd():
temp_git_dir.mkdir('dir').join('x').write('foo')
temp_git_dir.mkdir('DIR').join('y').write('foo')
cmd_output('git', 'add', '-A')
assert find_conflicting_filenames([]) == 1
@skip_win32 # pragma: win32 no cover
def test_adding_files_with_conflicting_deep_directories(temp_git_dir):
with temp_git_dir.as_cwd():
temp_git_dir.mkdir('x').mkdir('y').join('z').write('foo')
temp_git_dir.join('X').write('foo')
cmd_output('git', 'add', '-A')
assert find_conflicting_filenames([]) == 1
@skip_win32 # pragma: win32 no cover
def test_adding_file_with_conflicting_directory(temp_git_dir):
with temp_git_dir.as_cwd():
temp_git_dir.mkdir('dir').join('x').write('foo')
temp_git_dir.join('DIR').write('foo')
cmd_output('git', 'add', '-A')
assert find_conflicting_filenames([]) == 1
def test_added_file_not_in_pre_commits_list(temp_git_dir): def test_added_file_not_in_pre_commits_list(temp_git_dir):
with temp_git_dir.as_cwd(): with temp_git_dir.as_cwd():
temp_git_dir.join('f.py').write("print('hello world')") temp_git_dir.join('f.py').write("print('hello world')")
@ -88,7 +41,7 @@ def test_file_conflicts_with_committed_file(temp_git_dir):
with temp_git_dir.as_cwd(): with temp_git_dir.as_cwd():
temp_git_dir.join('f.py').write("print('hello world')") temp_git_dir.join('f.py').write("print('hello world')")
cmd_output('git', 'add', 'f.py') cmd_output('git', 'add', 'f.py')
git_commit('-m', 'Add f.py') cmd_output('git', 'commit', '--no-gpg-sign', '-n', '-m', 'Add f.py')
temp_git_dir.join('F.py').write("print('hello world')") temp_git_dir.join('F.py').write("print('hello world')")
cmd_output('git', 'add', 'F.py') cmd_output('git', 'add', 'F.py')
@ -96,19 +49,6 @@ def test_file_conflicts_with_committed_file(temp_git_dir):
assert find_conflicting_filenames(['F.py']) == 1 assert find_conflicting_filenames(['F.py']) == 1
@skip_win32 # pragma: win32 no cover
def test_file_conflicts_with_committed_dir(temp_git_dir):
with temp_git_dir.as_cwd():
temp_git_dir.mkdir('dir').join('x').write('foo')
cmd_output('git', 'add', '-A')
git_commit('-m', 'Add f.py')
temp_git_dir.join('DIR').write('foo')
cmd_output('git', 'add', '-A')
assert find_conflicting_filenames([]) == 1
def test_integration(temp_git_dir): def test_integration(temp_git_dir):
with temp_git_dir.as_cwd(): with temp_git_dir.as_cwd():
assert main(argv=[]) == 0 assert main(argv=[]) == 0

View file

@ -1,4 +1,6 @@
from __future__ import annotations # -*- coding: utf-8 -*-
from __future__ import absolute_import
from __future__ import unicode_literals
import pytest import pytest
@ -17,7 +19,7 @@ TESTS = (
b'from __future__ import unicode_literals\n' b'from __future__ import unicode_literals\n'
b'"foo"\n', b'"foo"\n',
1, 1,
'{filename}:2: Module docstring appears after code ' '{filename}:2 Module docstring appears after code '
'(code seen on line 1).\n', '(code seen on line 1).\n',
), ),
# Test double docstring # Test double docstring
@ -26,7 +28,7 @@ TESTS = (
b'from __future__ import absolute_import\n' b'from __future__ import absolute_import\n'
b'"fake docstring"\n', b'"fake docstring"\n',
1, 1,
'{filename}:3: Multiple module docstrings ' '{filename}:3 Multiple module docstrings '
'(first docstring on line 1).\n', '(first docstring on line 1).\n',
), ),
# Test multiple lines of code above # Test multiple lines of code above
@ -35,7 +37,7 @@ TESTS = (
b'import sys\n' b'import sys\n'
b'"docstring"\n', b'"docstring"\n',
1, 1,
'{filename}:3: Module docstring appears after code ' '{filename}:3 Module docstring appears after code '
'(code seen on line 1).\n', '(code seen on line 1).\n',
), ),
# String literals in expressions are ok. # String literals in expressions are ok.
@ -58,12 +60,12 @@ def test_unit(capsys, contents, expected, expected_out):
def test_integration(tmpdir, capsys, contents, expected, expected_out): def test_integration(tmpdir, capsys, contents, expected, expected_out):
f = tmpdir.join('test.py') f = tmpdir.join('test.py')
f.write_binary(contents) f.write_binary(contents)
assert main([str(f)]) == expected assert main([f.strpath]) == expected
assert capsys.readouterr()[0] == expected_out.format(filename=str(f)) assert capsys.readouterr()[0] == expected_out.format(filename=f.strpath)
def test_arbitrary_encoding(tmpdir): def test_arbitrary_encoding(tmpdir):
f = tmpdir.join('f.py') f = tmpdir.join('f.py')
contents = '# -*- coding: cp1252\nx = "£"'.encode('cp1252') contents = '# -*- coding: cp1252\nx = "£"'.encode('cp1252')
f.write_binary(contents) f.write_binary(contents)
assert main([str(f)]) == 0 assert main([f.strpath]) == 0

View file

@ -1,127 +1,39 @@
from __future__ import annotations # -*- coding: utf-8 -*-
from __future__ import absolute_import
import os from __future__ import unicode_literals
import sys
import pytest import pytest
from pre_commit_hooks import check_executables_have_shebangs
from pre_commit_hooks.check_executables_have_shebangs import main from pre_commit_hooks.check_executables_have_shebangs import main
from pre_commit_hooks.util import cmd_output
skip_win32 = pytest.mark.skipif(
sys.platform == 'win32',
reason="non-git checks aren't relevant on windows",
)
@skip_win32 # pragma: win32 no cover
@pytest.mark.parametrize( @pytest.mark.parametrize(
'content', ( 'content', (
b'#!/bin/bash\nhello world\n', b'#!/bin/bash\nhello world\n',
b'#!/usr/bin/env python3.6', b'#!/usr/bin/env python3.6',
b'#!python', b'#!python',
'#!☃'.encode(), '#!☃'.encode('UTF-8'),
), ),
) )
def test_has_shebang(content, tmpdir): def test_has_shebang(content, tmpdir):
path = tmpdir.join('path') path = tmpdir.join('path')
path.write(content, 'wb') path.write(content, 'wb')
assert main((str(path),)) == 0 assert main((path.strpath,)) == 0
@skip_win32 # pragma: win32 no cover
@pytest.mark.parametrize( @pytest.mark.parametrize(
'content', ( 'content', (
b'', b'',
b' #!python\n', b' #!python\n',
b'\n#!python\n', b'\n#!python\n',
b'python\n', b'python\n',
''.encode(), ''.encode('UTF-8'),
), ),
) )
def test_bad_shebang(content, tmpdir, capsys): def test_bad_shebang(content, tmpdir, capsys):
path = tmpdir.join('path') path = tmpdir.join('path')
path.write(content, 'wb') path.write(content, 'wb')
assert main((str(path),)) == 1 assert main((path.strpath,)) == 1
_, stderr = capsys.readouterr() _, stderr = capsys.readouterr()
assert stderr.startswith(f'{path}: marked executable but') assert stderr.startswith('{}: marked executable but'.format(path.strpath))
def test_check_git_filemode_passing(tmpdir):
with tmpdir.as_cwd():
cmd_output('git', 'init', '.')
f = tmpdir.join('f')
f.write('#!/usr/bin/env bash')
f_path = str(f)
cmd_output('chmod', '+x', f_path)
cmd_output('git', 'add', f_path)
cmd_output('git', 'update-index', '--chmod=+x', f_path)
g = tmpdir.join('g').ensure()
g_path = str(g)
cmd_output('git', 'add', g_path)
# this is potentially a problem, but not something the script intends
# to check for -- we're only making sure that things that are
# executable have shebangs
h = tmpdir.join('h')
h.write('#!/usr/bin/env bash')
h_path = str(h)
cmd_output('git', 'add', h_path)
files = (f_path, g_path, h_path)
assert check_executables_have_shebangs._check_git_filemode(files) == 0
def test_check_git_filemode_passing_unusual_characters(tmpdir):
with tmpdir.as_cwd():
cmd_output('git', 'init', '.')
f = tmpdir.join('mañana.txt')
f.write('#!/usr/bin/env bash')
f_path = str(f)
cmd_output('chmod', '+x', f_path)
cmd_output('git', 'add', f_path)
cmd_output('git', 'update-index', '--chmod=+x', f_path)
files = (f_path,)
assert check_executables_have_shebangs._check_git_filemode(files) == 0
def test_check_git_filemode_failing(tmpdir):
with tmpdir.as_cwd():
cmd_output('git', 'init', '.')
f = tmpdir.join('f').ensure()
f_path = str(f)
cmd_output('chmod', '+x', f_path)
cmd_output('git', 'add', f_path)
cmd_output('git', 'update-index', '--chmod=+x', f_path)
files = (f_path,)
assert check_executables_have_shebangs._check_git_filemode(files) == 1
@pytest.mark.parametrize(
('content', 'mode', 'expected'),
(
pytest.param('#!python', '+x', 0, id='shebang with executable'),
pytest.param('#!python', '-x', 0, id='shebang without executable'),
pytest.param('', '+x', 1, id='no shebang with executable'),
pytest.param('', '-x', 0, id='no shebang without executable'),
),
)
def test_git_executable_shebang(temp_git_dir, content, mode, expected):
with temp_git_dir.as_cwd():
path = temp_git_dir.join('path')
path.write(content)
cmd_output('git', 'add', str(path))
cmd_output('chmod', mode, str(path))
cmd_output('git', 'update-index', f'--chmod={mode}', str(path))
# simulate how identify chooses that something is executable
filenames = [path for path in [str(path)] if os.access(path, os.X_OK)]
assert main(filenames) == expected

View file

@ -1,63 +0,0 @@
from __future__ import annotations
import os.path
import re
import pytest
from pre_commit_hooks.check_yaml import yaml
@pytest.fixture(scope='module')
def hook_re():
here = os.path.dirname(__file__)
with open(os.path.join(here, '..', '.pre-commit-hooks.yaml')) as f:
hook_defs = yaml.load(f)
hook, = (
hook
for hook in hook_defs
if hook['id'] == 'check-illegal-windows-names'
)
yield re.compile(hook['files'])
@pytest.mark.parametrize(
's',
(
pytest.param('aux.txt', id='with ext'),
pytest.param('aux', id='without ext'),
pytest.param('AuX.tXt', id='capitals'),
pytest.param('com7.dat', id='com with digit'),
pytest.param(':', id='bare colon'),
pytest.param('file:Zone.Identifier', id='mid colon'),
pytest.param('path/COM¹.json', id='com with superscript'),
pytest.param('dir/LPT³.toml', id='lpt with superscript'),
pytest.param('with < less than', id='with less than'),
pytest.param('Fast or Slow?.md', id='with question mark'),
pytest.param('with "double" quotes', id='with double quotes'),
pytest.param('with_null\x00byte', id='with null byte'),
pytest.param('ends_with.', id='ends with period'),
pytest.param('ends_with ', id='ends with space'),
pytest.param('ends_with\t', id='ends with tab'),
pytest.param('dir/ends./with.txt', id='directory ends with period'),
pytest.param('dir/ends /with.txt', id='directory ends with space'),
),
)
def test_check_illegal_windows_names_matches(hook_re, s):
assert hook_re.search(s)
@pytest.mark.parametrize(
's',
(
pytest.param('README.md', id='standard file'),
pytest.param('foo.aux', id='as ext'),
pytest.param('com.dat', id='com without digit'),
pytest.param('.python-version', id='starts with period'),
pytest.param(' pseudo nan', id='with spaces'),
pytest.param('!@#$%^&;=≤\'~`¡¿€🤗', id='with allowed characters'),
pytest.param('path.to/file.py', id='standard path'),
),
)
def test_check_illegal_windows_names_does_not_match(hook_re, s):
assert hook_re.search(s) is None

View file

@ -1,5 +1,3 @@
from __future__ import annotations
import pytest import pytest
from pre_commit_hooks.check_json import main from pre_commit_hooks.check_json import main
@ -11,7 +9,6 @@ from testing.util import get_resource_path
('bad_json.notjson', 1), ('bad_json.notjson', 1),
('bad_json_latin1.nonjson', 1), ('bad_json_latin1.nonjson', 1),
('ok_json.json', 0), ('ok_json.json', 0),
('duplicate_key_json.notjson', 1),
), ),
) )
def test_main(capsys, filename, expected_retval): def test_main(capsys, filename, expected_retval):
@ -20,9 +17,3 @@ def test_main(capsys, filename, expected_retval):
if expected_retval == 1: if expected_retval == 1:
stdout, _ = capsys.readouterr() stdout, _ = capsys.readouterr()
assert filename in stdout assert filename in stdout
def test_non_utf8_file(tmpdir):
f = tmpdir.join('t.json')
f.write_binary(b'\xa9\xfe\x12')
assert main((str(f),))

View file

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import absolute_import
from __future__ import unicode_literals
import os import os
import shutil import shutil
@ -8,7 +9,6 @@ import pytest
from pre_commit_hooks.check_merge_conflict import main from pre_commit_hooks.check_merge_conflict import main
from pre_commit_hooks.util import cmd_output from pre_commit_hooks.util import cmd_output
from testing.util import get_resource_path from testing.util import get_resource_path
from testing.util import git_commit
@pytest.fixture @pytest.fixture
@ -19,23 +19,23 @@ def f1_is_a_conflict_file(tmpdir):
repo2 = tmpdir.join('repo2') repo2 = tmpdir.join('repo2')
repo2_f1 = repo2.join('f1') repo2_f1 = repo2.join('f1')
cmd_output('git', 'init', '--', str(repo1)) cmd_output('git', 'init', '--', repo1.strpath)
with repo1.as_cwd(): with repo1.as_cwd():
repo1_f1.ensure() repo1_f1.ensure()
cmd_output('git', 'add', '.') cmd_output('git', 'add', '.')
git_commit('-m', 'commit1') cmd_output('git', 'commit', '--no-gpg-sign', '-m', 'commit1')
cmd_output('git', 'clone', str(repo1), str(repo2)) cmd_output('git', 'clone', repo1.strpath, repo2.strpath)
# Commit in mainline # Commit in master
with repo1.as_cwd(): with repo1.as_cwd():
repo1_f1.write('parent\n') repo1_f1.write('parent\n')
git_commit('-am', 'mainline commit2') cmd_output('git', 'commit', '--no-gpg-sign', '-am', 'master commit2')
# Commit in clone and pull # Commit in clone and pull
with repo2.as_cwd(): with repo2.as_cwd():
repo2_f1.write('child\n') repo2_f1.write('child\n')
git_commit('-am', 'clone commit2') cmd_output('git', 'commit', '--no-gpg-sign', '-am', 'clone commit2')
cmd_output('git', 'pull', '--no-rebase', retcode=None) cmd_output('git', 'pull', '--no-rebase', retcode=None)
# We should end up in a merge conflict! # We should end up in a merge conflict!
f1 = repo2_f1.read() f1 = repo2_f1.read()
@ -74,24 +74,24 @@ def repository_pending_merge(tmpdir):
repo2 = tmpdir.join('repo2') repo2 = tmpdir.join('repo2')
repo2_f1 = repo2.join('f1') repo2_f1 = repo2.join('f1')
repo2_f2 = repo2.join('f2') repo2_f2 = repo2.join('f2')
cmd_output('git', 'init', str(repo1)) cmd_output('git', 'init', repo1.strpath)
with repo1.as_cwd(): with repo1.as_cwd():
repo1_f1.ensure() repo1_f1.ensure()
cmd_output('git', 'add', '.') cmd_output('git', 'add', '.')
git_commit('-m', 'commit1') cmd_output('git', 'commit', '--no-gpg-sign', '-m', 'commit1')
cmd_output('git', 'clone', str(repo1), str(repo2)) cmd_output('git', 'clone', repo1.strpath, repo2.strpath)
# Commit in mainline # Commit in master
with repo1.as_cwd(): with repo1.as_cwd():
repo1_f1.write('parent\n') repo1_f1.write('parent\n')
git_commit('-am', 'mainline commit2') cmd_output('git', 'commit', '--no-gpg-sign', '-am', 'master commit2')
# Commit in clone and pull without committing # Commit in clone and pull without committing
with repo2.as_cwd(): with repo2.as_cwd():
repo2_f2.write('child\n') repo2_f2.write('child\n')
cmd_output('git', 'add', '.') cmd_output('git', 'add', '.')
git_commit('-m', 'clone commit2') cmd_output('git', 'commit', '--no-gpg-sign', '-m', 'clone commit2')
cmd_output('git', 'pull', '--no-commit', '--no-rebase') cmd_output('git', 'pull', '--no-commit', '--no-rebase')
# We should end up in a pending merge # We should end up in a pending merge
assert repo2_f1.read() == 'parent\n' assert repo2_f1.read() == 'parent\n'
@ -101,18 +101,12 @@ def repository_pending_merge(tmpdir):
@pytest.mark.usefixtures('f1_is_a_conflict_file') @pytest.mark.usefixtures('f1_is_a_conflict_file')
def test_merge_conflicts_git(capsys): def test_merge_conflicts_git():
assert main(['f1']) == 1 assert main(['f1']) == 1
out, _ = capsys.readouterr()
assert out == (
"f1:1: Merge conflict string '<<<<<<<' found\n"
"f1:3: Merge conflict string '=======' found\n"
"f1:5: Merge conflict string '>>>>>>>' found\n"
)
@pytest.mark.parametrize( @pytest.mark.parametrize(
'contents', (b'<<<<<<< HEAD\n', b'=======\n', b'>>>>>>> main\n'), 'contents', (b'<<<<<<< HEAD\n', b'=======\n', b'>>>>>>> master\n'),
) )
def test_merge_conflicts_failing(contents, repository_pending_merge): def test_merge_conflicts_failing(contents, repository_pending_merge):
repository_pending_merge.join('f2').write_binary(contents) repository_pending_merge.join('f2').write_binary(contents)
@ -143,15 +137,3 @@ def test_care_when_assumed_merge(tmpdir):
f = tmpdir.join('README.md') f = tmpdir.join('README.md')
f.write_binary(b'problem\n=======\n') f.write_binary(b'problem\n=======\n')
assert main([str(f.realpath()), '--assume-in-merge']) == 1 assert main([str(f.realpath()), '--assume-in-merge']) == 1
def test_worktree_merge_conflicts(f1_is_a_conflict_file, tmpdir, capsys):
worktree = tmpdir.join('worktree')
cmd_output('git', 'worktree', 'add', str(worktree))
with worktree.as_cwd():
cmd_output(
'git', 'pull', '--no-rebase', 'origin', 'HEAD', retcode=None,
)
msg = f1_is_a_conflict_file.join('.git/worktrees/worktree/MERGE_MSG')
assert msg.exists()
test_merge_conflicts_git(capsys)

View file

@ -1,89 +0,0 @@
from __future__ import annotations
import os
import pytest
from pre_commit_hooks.check_shebang_scripts_are_executable import \
_check_git_filemode
from pre_commit_hooks.check_shebang_scripts_are_executable import main
from pre_commit_hooks.util import cmd_output
def test_check_git_filemode_passing(tmpdir):
with tmpdir.as_cwd():
cmd_output('git', 'init', '.')
f = tmpdir.join('f')
f.write('#!/usr/bin/env bash')
f_path = str(f)
cmd_output('chmod', '+x', f_path)
cmd_output('git', 'add', f_path)
cmd_output('git', 'update-index', '--chmod=+x', f_path)
g = tmpdir.join('g').ensure()
g_path = str(g)
cmd_output('git', 'add', g_path)
files = [f_path, g_path]
assert _check_git_filemode(files) == 0
# this is the one we should trigger on
h = tmpdir.join('h')
h.write('#!/usr/bin/env bash')
h_path = str(h)
cmd_output('git', 'add', h_path)
files = [h_path]
assert _check_git_filemode(files) == 1
def test_check_git_filemode_passing_unusual_characters(tmpdir):
with tmpdir.as_cwd():
cmd_output('git', 'init', '.')
f = tmpdir.join('mañana.txt')
f.write('#!/usr/bin/env bash')
f_path = str(f)
cmd_output('chmod', '+x', f_path)
cmd_output('git', 'add', f_path)
cmd_output('git', 'update-index', '--chmod=+x', f_path)
files = (f_path,)
assert _check_git_filemode(files) == 0
def test_check_git_filemode_failing(tmpdir):
with tmpdir.as_cwd():
cmd_output('git', 'init', '.')
f = tmpdir.join('f').ensure()
f.write('#!/usr/bin/env bash')
f_path = str(f)
cmd_output('git', 'add', f_path)
files = (f_path,)
assert _check_git_filemode(files) == 1
@pytest.mark.parametrize(
('content', 'mode', 'expected'),
(
pytest.param('#!python', '+x', 0, id='shebang with executable'),
pytest.param('#!python', '-x', 1, id='shebang without executable'),
pytest.param('', '+x', 0, id='no shebang with executable'),
pytest.param('', '-x', 0, id='no shebang without executable'),
),
)
def test_git_executable_shebang(temp_git_dir, content, mode, expected):
with temp_git_dir.as_cwd():
path = temp_git_dir.join('path')
path.write(content)
cmd_output('git', 'add', str(path))
cmd_output('chmod', mode, str(path))
cmd_output('git', 'update-index', f'--chmod={mode}', str(path))
# simulate how identify chooses that something is executable
filenames = [path for path in [str(path)] if os.access(path, os.X_OK)]
assert main(filenames) == expected

View file

@ -1,5 +1,3 @@
from __future__ import annotations
import os import os
import pytest import pytest
@ -18,8 +16,8 @@ def test_main(tmpdir, dest, expected): # pragma: no cover (symlinks)
tmpdir.join('exists').ensure() tmpdir.join('exists').ensure()
symlink = tmpdir.join('symlink') symlink = tmpdir.join('symlink')
symlink.mksymlinkto(tmpdir.join(dest)) symlink.mksymlinkto(tmpdir.join(dest))
assert main((str(symlink),)) == expected assert main((symlink.strpath,)) == expected
def test_main_normal_file(tmpdir): def test_main_normal_file(tmpdir):
assert main((str(tmpdir.join('f').ensure()),)) == 0 assert main((tmpdir.join('f').ensure().strpath,)) == 0

View file

@ -1,20 +1,21 @@
from __future__ import annotations from __future__ import absolute_import
from __future__ import unicode_literals
from pre_commit_hooks.check_toml import main from pre_commit_hooks.check_toml import main
def test_toml_bad(tmpdir): def test_toml_good(tmpdir):
filename = tmpdir.join('f') filename = tmpdir.join('f')
filename.write(""" filename.write("""
key = # INVALID key = # INVALID
= "no key name" # INVALID = "no key name" # INVALID
""") """)
ret = main((str(filename),)) ret = main((filename.strpath,))
assert ret == 1 assert ret == 1
def test_toml_good(tmpdir): def test_toml_bad(tmpdir):
filename = tmpdir.join('f') filename = tmpdir.join('f')
filename.write( filename.write(
""" """
@ -27,12 +28,5 @@ name = "John"
dob = 1979-05-27T07:32:00-08:00 # First class dates dob = 1979-05-27T07:32:00-08:00 # First class dates
""", """,
) )
ret = main((str(filename),)) ret = main((filename.strpath,))
assert ret == 0
def test_toml_good_unicode(tmpdir):
filename = tmpdir.join('f')
filename.write_binary('letter = "\N{SNOWMAN}"\n'.encode())
ret = main((str(filename),))
assert ret == 0 assert ret == 0

View file

@ -1,11 +1,12 @@
from __future__ import annotations from __future__ import absolute_import
from __future__ import unicode_literals
from pre_commit_hooks.check_vcs_permalinks import main from pre_commit_hooks.check_vcs_permalinks import main
def test_trivial(tmpdir): def test_trivial(tmpdir):
f = tmpdir.join('f.txt').ensure() f = tmpdir.join('f.txt').ensure()
assert not main((str(f),)) assert not main((f.strpath,))
def test_passing(tmpdir): def test_passing(tmpdir):
@ -13,28 +14,24 @@ def test_passing(tmpdir):
f.write_binary( f.write_binary(
# permalinks are ok # permalinks are ok
b'https://github.com/asottile/test/blob/649e6/foo%20bar#L1\n' b'https://github.com/asottile/test/blob/649e6/foo%20bar#L1\n'
# tags are ok
b'https://github.com/asottile/test/blob/1.0.0/foo%20bar#L1\n'
# links to files but not line numbers are ok # links to files but not line numbers are ok
b'https://github.com/asottile/test/blob/main/foo%20bar\n' b'https://github.com/asottile/test/blob/master/foo%20bar\n'
# regression test for overly-greedy regex # regression test for overly-greedy regex
b'https://github.com/ yes / no ? /blob/main/foo#L1\n', b'https://github.com/ yes / no ? /blob/master/foo#L1\n',
) )
assert not main((str(f),)) assert not main((f.strpath,))
def test_failing(tmpdir, capsys): def test_failing(tmpdir, capsys):
with tmpdir.as_cwd(): with tmpdir.as_cwd():
tmpdir.join('f.txt').write_binary( tmpdir.join('f.txt').write_binary(
b'https://github.com/asottile/test/blob/main/foo#L1\n' b'https://github.com/asottile/test/blob/master/foo#L1\n',
b'https://example.com/asottile/test/blob/main/foo#L1\n',
) )
assert main(('f.txt', '--additional-github-domain', 'example.com')) assert main(('f.txt',))
out, _ = capsys.readouterr() out, _ = capsys.readouterr()
assert out == ( assert out == (
'f.txt:1:https://github.com/asottile/test/blob/main/foo#L1\n' 'f.txt:1:https://github.com/asottile/test/blob/master/foo#L1\n'
'f.txt:2:https://example.com/asottile/test/blob/main/foo#L1\n'
'\n' '\n'
'Non-permanent github link detected.\n' 'Non-permanent github link detected.\n'
'On any page on github press [y] to load a permalink.\n' 'On any page on github press [y] to load a permalink.\n'

View file

@ -1,5 +1,3 @@
from __future__ import annotations
import pytest import pytest
from pre_commit_hooks.check_xml import main from pre_commit_hooks.check_xml import main

View file

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import absolute_import
from __future__ import unicode_literals
import pytest import pytest
@ -22,16 +23,16 @@ def test_main_allow_multiple_documents(tmpdir):
f.write('---\nfoo\n---\nbar\n') f.write('---\nfoo\n---\nbar\n')
# should fail without the setting # should fail without the setting
assert main((str(f),)) assert main((f.strpath,))
# should pass when we allow multiple documents # should pass when we allow multiple documents
assert not main(('--allow-multiple-documents', str(f))) assert not main(('--allow-multiple-documents', f.strpath))
def test_fails_even_with_allow_multiple_documents(tmpdir): def test_fails_even_with_allow_multiple_documents(tmpdir):
f = tmpdir.join('test.yaml') f = tmpdir.join('test.yaml')
f.write('[') f.write('[')
assert main(('--allow-multiple-documents', str(f))) assert main(('--allow-multiple-documents', f.strpath))
def test_main_unsafe(tmpdir): def test_main_unsafe(tmpdir):
@ -42,12 +43,12 @@ def test_main_unsafe(tmpdir):
' deadbeefdeadbeefdeadbeef\n', ' deadbeefdeadbeefdeadbeef\n',
) )
# should fail "safe" check # should fail "safe" check
assert main((str(f),)) assert main((f.strpath,))
# should pass when we allow unsafe documents # should pass when we allow unsafe documents
assert not main(('--unsafe', str(f))) assert not main(('--unsafe', f.strpath))
def test_main_unsafe_still_fails_on_syntax_errors(tmpdir): def test_main_unsafe_still_fails_on_syntax_errors(tmpdir):
f = tmpdir.join('test.yaml') f = tmpdir.join('test.yaml')
f.write('[') f.write('[')
assert main(('--unsafe', str(f))) assert main(('--unsafe', f.strpath))

View file

@ -1,4 +1,6 @@
from __future__ import annotations from __future__ import absolute_import
from __future__ import print_function
from __future__ import unicode_literals
import pytest import pytest
@ -8,5 +10,5 @@ from pre_commit_hooks.util import cmd_output
@pytest.fixture @pytest.fixture
def temp_git_dir(tmpdir): def temp_git_dir(tmpdir):
git_dir = tmpdir.join('gits') git_dir = tmpdir.join('gits')
cmd_output('git', 'init', '--', str(git_dir)) cmd_output('git', 'init', '--', git_dir.strpath)
yield git_dir yield git_dir

View file

@ -1,4 +1,6 @@
from __future__ import annotations # -*- coding: utf-8 -*-
from __future__ import absolute_import
from __future__ import unicode_literals
import ast import ast
@ -35,7 +37,7 @@ def test_finds_breakpoint():
def test_returns_one_for_failing_file(tmpdir): def test_returns_one_for_failing_file(tmpdir):
f_py = tmpdir.join('f.py') f_py = tmpdir.join('f.py')
f_py.write('def f():\n import pdb; pdb.set_trace()') f_py.write('def f():\n import pdb; pdb.set_trace()')
ret = main([str(f_py)]) ret = main([f_py.strpath])
assert ret == 1 assert ret == 1
@ -52,12 +54,10 @@ def test_syntaxerror_file():
def test_non_utf8_file(tmpdir): def test_non_utf8_file(tmpdir):
f_py = tmpdir.join('f.py') f_py = tmpdir.join('f.py')
f_py.write_binary('# -*- coding: cp1252 -*-\nx = ""\n'.encode('cp1252')) f_py.write_binary('# -*- coding: cp1252 -*-\nx = ""\n'.encode('cp1252'))
assert main((str(f_py),)) == 0 assert main((f_py.strpath,)) == 0
def test_py37_breakpoint(tmpdir, capsys): def test_py37_breakpoint(tmpdir):
f_py = tmpdir.join('f.py') f_py = tmpdir.join('f.py')
f_py.write('def f():\n breakpoint()\n') f_py.write('def f():\n breakpoint()\n')
assert main((str(f_py),)) == 1 assert main((f_py.strpath,)) == 1
out, _ = capsys.readouterr()
assert out == f'{f_py}:2:4: breakpoint called\n'

View file

@ -1,75 +0,0 @@
from __future__ import annotations
import os
import subprocess
import pytest
from pre_commit_hooks.destroyed_symlinks import find_destroyed_symlinks
from pre_commit_hooks.destroyed_symlinks import main
from testing.util import git_commit
TEST_SYMLINK = 'test_symlink'
TEST_SYMLINK_TARGET = '/doesnt/really/matters'
TEST_FILE = 'test_file'
TEST_FILE_RENAMED = f'{TEST_FILE}_renamed'
@pytest.fixture
def repo_with_destroyed_symlink(tmpdir):
source_repo = tmpdir.join('src')
os.makedirs(source_repo, exist_ok=True)
test_repo = tmpdir.join('test')
with source_repo.as_cwd():
subprocess.check_call(('git', 'init'))
os.symlink(TEST_SYMLINK_TARGET, TEST_SYMLINK)
with open(TEST_FILE, 'w') as f:
print('some random content', file=f)
subprocess.check_call(('git', 'add', '.'))
git_commit('-m', 'initial')
assert b'120000 ' in subprocess.check_output(
('git', 'cat-file', '-p', 'HEAD^{tree}'),
)
subprocess.check_call(
('git', '-c', 'core.symlinks=false', 'clone', source_repo, test_repo),
)
with test_repo.as_cwd():
subprocess.check_call(
('git', 'config', '--local', 'core.symlinks', 'true'),
)
subprocess.check_call(('git', 'mv', TEST_FILE, TEST_FILE_RENAMED))
assert not os.path.islink(test_repo.join(TEST_SYMLINK))
yield test_repo
def test_find_destroyed_symlinks(repo_with_destroyed_symlink):
with repo_with_destroyed_symlink.as_cwd():
assert find_destroyed_symlinks([]) == []
assert main([]) == 0
subprocess.check_call(('git', 'add', TEST_SYMLINK))
assert find_destroyed_symlinks([TEST_SYMLINK]) == [TEST_SYMLINK]
assert find_destroyed_symlinks([]) == []
assert main([]) == 0
assert find_destroyed_symlinks([TEST_FILE_RENAMED, TEST_FILE]) == []
ALL_STAGED = [TEST_SYMLINK, TEST_FILE_RENAMED]
assert find_destroyed_symlinks(ALL_STAGED) == [TEST_SYMLINK]
assert main(ALL_STAGED) != 0
with open(TEST_SYMLINK, 'a') as f:
print(file=f) # add trailing newline
subprocess.check_call(['git', 'add', TEST_SYMLINK])
assert find_destroyed_symlinks(ALL_STAGED) == [TEST_SYMLINK]
assert main(ALL_STAGED) != 0
with open(TEST_SYMLINK, 'w') as f:
print('0' * len(TEST_SYMLINK_TARGET), file=f)
subprocess.check_call(('git', 'add', TEST_SYMLINK))
assert find_destroyed_symlinks(ALL_STAGED) == []
assert main(ALL_STAGED) == 0
with open(TEST_SYMLINK, 'w') as f:
print('0' * (len(TEST_SYMLINK_TARGET) + 3), file=f)
subprocess.check_call(('git', 'add', TEST_SYMLINK))
assert find_destroyed_symlinks(ALL_STAGED) == []
assert main(ALL_STAGED) == 0

View file

@ -1,8 +1,5 @@
from __future__ import annotations
from unittest.mock import patch
import pytest import pytest
from mock import patch
from pre_commit_hooks.detect_aws_credentials import get_aws_cred_files_from_env from pre_commit_hooks.detect_aws_credentials import get_aws_cred_files_from_env
from pre_commit_hooks.detect_aws_credentials import get_aws_secrets_from_env from pre_commit_hooks.detect_aws_credentials import get_aws_secrets_from_env
@ -15,15 +12,15 @@ from testing.util import get_resource_path
('env_vars', 'values'), ('env_vars', 'values'),
( (
({}, set()), ({}, set()),
({'AWS_PLACEHOLDER_KEY': '/foo'}, set()), ({'AWS_DUMMY_KEY': '/foo'}, set()),
({'AWS_CONFIG_FILE': '/foo'}, {'/foo'}), ({'AWS_CONFIG_FILE': '/foo'}, {'/foo'}),
({'AWS_CREDENTIAL_FILE': '/foo'}, {'/foo'}), ({'AWS_CREDENTIAL_FILE': '/foo'}, {'/foo'}),
({'AWS_SHARED_CREDENTIALS_FILE': '/foo'}, {'/foo'}), ({'AWS_SHARED_CREDENTIALS_FILE': '/foo'}, {'/foo'}),
({'BOTO_CONFIG': '/foo'}, {'/foo'}), ({'BOTO_CONFIG': '/foo'}, {'/foo'}),
({'AWS_PLACEHOLDER_KEY': '/foo', 'AWS_CONFIG_FILE': '/bar'}, {'/bar'}), ({'AWS_DUMMY_KEY': '/foo', 'AWS_CONFIG_FILE': '/bar'}, {'/bar'}),
( (
{ {
'AWS_PLACEHOLDER_KEY': '/foo', 'AWS_CONFIG_FILE': '/bar', 'AWS_DUMMY_KEY': '/foo', 'AWS_CONFIG_FILE': '/bar',
'AWS_CREDENTIAL_FILE': '/baz', 'AWS_CREDENTIAL_FILE': '/baz',
}, },
{'/bar', '/baz'}, {'/bar', '/baz'},
@ -46,16 +43,11 @@ def test_get_aws_credentials_file_from_env(env_vars, values):
('env_vars', 'values'), ('env_vars', 'values'),
( (
({}, set()), ({}, set()),
({'AWS_PLACEHOLDER_KEY': 'foo'}, set()), ({'AWS_DUMMY_KEY': 'foo'}, set()),
({'AWS_SECRET_ACCESS_KEY': 'foo'}, {'foo'}), ({'AWS_SECRET_ACCESS_KEY': 'foo'}, {'foo'}),
({'AWS_SECURITY_TOKEN': 'foo'}, {'foo'}), ({'AWS_SECURITY_TOKEN': 'foo'}, {'foo'}),
({'AWS_SESSION_TOKEN': 'foo'}, {'foo'}), ({'AWS_SESSION_TOKEN': 'foo'}, {'foo'}),
({'AWS_SESSION_TOKEN': ''}, set()), ({'AWS_DUMMY_KEY': 'foo', 'AWS_SECRET_ACCESS_KEY': 'bar'}, {'bar'}),
({'AWS_SESSION_TOKEN': 'foo', 'AWS_SECURITY_TOKEN': ''}, {'foo'}),
(
{'AWS_PLACEHOLDER_KEY': 'foo', 'AWS_SECRET_ACCESS_KEY': 'bar'},
{'bar'},
),
( (
{'AWS_SECRET_ACCESS_KEY': 'foo', 'AWS_SECURITY_TOKEN': 'bar'}, {'AWS_SECRET_ACCESS_KEY': 'foo', 'AWS_SECURITY_TOKEN': 'bar'},
{'foo', 'bar'}, {'foo', 'bar'},
@ -123,19 +115,6 @@ def test_detect_aws_credentials(filename, expected_retval):
assert ret == expected_retval assert ret == expected_retval
def test_allows_arbitrarily_encoded_files(tmpdir):
src_ini = tmpdir.join('src.ini')
src_ini.write(
'[default]\n'
'aws_access_key_id=AKIASDFASDF\n'
'aws_secret_Access_key=9018asdf23908190238123\n',
)
arbitrary_encoding = tmpdir.join('f')
arbitrary_encoding.write_binary(b'\x12\x9a\xe2\xf2')
ret = main((str(arbitrary_encoding), '--credentials-file', str(src_ini)))
assert ret == 0
@patch('pre_commit_hooks.detect_aws_credentials.get_aws_secrets_from_file') @patch('pre_commit_hooks.detect_aws_credentials.get_aws_secrets_from_file')
@patch('pre_commit_hooks.detect_aws_credentials.get_aws_secrets_from_env') @patch('pre_commit_hooks.detect_aws_credentials.get_aws_secrets_from_env')
def test_non_existent_credentials(mock_secrets_env, mock_secrets_file, capsys): def test_non_existent_credentials(mock_secrets_env, mock_secrets_file, capsys):

View file

@ -1,5 +1,3 @@
from __future__ import annotations
import pytest import pytest
from pre_commit_hooks.detect_private_key import main from pre_commit_hooks.detect_private_key import main
@ -12,8 +10,6 @@ TESTS = (
(b'-----BEGIN OPENSSH PRIVATE KEY-----', 1), (b'-----BEGIN OPENSSH PRIVATE KEY-----', 1),
(b'PuTTY-User-Key-File-2: ssh-rsa', 1), (b'PuTTY-User-Key-File-2: ssh-rsa', 1),
(b'---- BEGIN SSH2 ENCRYPTED PRIVATE KEY ----', 1), (b'---- BEGIN SSH2 ENCRYPTED PRIVATE KEY ----', 1),
(b'-----BEGIN ENCRYPTED PRIVATE KEY-----', 1),
(b'-----BEGIN OpenVPN Static key V1-----', 1),
(b'ssh-rsa DATA', 0), (b'ssh-rsa DATA', 0),
(b'ssh-dsa DATA', 0), (b'ssh-dsa DATA', 0),
# Some arbitrary binary data # Some arbitrary binary data
@ -25,4 +21,4 @@ TESTS = (
def test_main(input_s, expected_retval, tmpdir): def test_main(input_s, expected_retval, tmpdir):
path = tmpdir.join('file.txt') path = tmpdir.join('file.txt')
path.write_binary(input_s) path.write_binary(input_s)
assert main([str(path)]) == expected_retval assert main([path.strpath]) == expected_retval

View file

@ -1,5 +1,3 @@
from __future__ import annotations
import io import io
import pytest import pytest
@ -37,7 +35,7 @@ def test_integration(input_s, expected_retval, output, tmpdir):
path = tmpdir.join('file.txt') path = tmpdir.join('file.txt')
path.write_binary(input_s) path.write_binary(input_s)
ret = main([str(path)]) ret = main([path.strpath])
file_output = path.read_binary() file_output = path.read_binary()
assert file_output == output assert file_output == output

View file

@ -1,5 +1,3 @@
from __future__ import annotations
import pytest import pytest
from pre_commit_hooks.file_contents_sorter import FAIL from pre_commit_hooks.file_contents_sorter import FAIL
@ -8,93 +6,28 @@ from pre_commit_hooks.file_contents_sorter import PASS
@pytest.mark.parametrize( @pytest.mark.parametrize(
('input_s', 'argv', 'expected_retval', 'output'), ('input_s', 'expected_retval', 'output'),
( (
(b'', [], PASS, b''), (b'', FAIL, b'\n'),
(b'\n', [], FAIL, b''), (b'lonesome\n', PASS, b'lonesome\n'),
(b'\n\n', [], FAIL, b''), (b'missing_newline', FAIL, b'missing_newline\n'),
(b'lonesome\n', [], PASS, b'lonesome\n'), (b'newline\nmissing', FAIL, b'missing\nnewline\n'),
(b'missing_newline', [], FAIL, b'missing_newline\n'), (b'missing\nnewline', FAIL, b'missing\nnewline\n'),
(b'newline\nmissing', [], FAIL, b'missing\nnewline\n'), (b'alpha\nbeta\n', PASS, b'alpha\nbeta\n'),
(b'missing\nnewline', [], FAIL, b'missing\nnewline\n'), (b'beta\nalpha\n', FAIL, b'alpha\nbeta\n'),
(b'alpha\nbeta\n', [], PASS, b'alpha\nbeta\n'), (b'C\nc\n', PASS, b'C\nc\n'),
(b'beta\nalpha\n', [], FAIL, b'alpha\nbeta\n'), (b'c\nC\n', FAIL, b'C\nc\n'),
(b'C\nc\n', [], PASS, b'C\nc\n'), (b'mag ical \n tre vor\n', FAIL, b' tre vor\nmag ical \n'),
(b'c\nC\n', [], FAIL, b'C\nc\n'), (b'@\n-\n_\n#\n', FAIL, b'#\n-\n@\n_\n'),
(b'mag ical \n tre vor\n', [], FAIL, b' tre vor\nmag ical \n'), (b'extra\n\n\nwhitespace\n', FAIL, b'extra\nwhitespace\n'),
(b'@\n-\n_\n#\n', [], FAIL, b'#\n-\n@\n_\n'), (b'whitespace\n\n\nextra\n', FAIL, b'extra\nwhitespace\n'),
(b'extra\n\n\nwhitespace\n', [], FAIL, b'extra\nwhitespace\n'),
(b'whitespace\n\n\nextra\n', [], FAIL, b'extra\nwhitespace\n'),
(
b'fee\nFie\nFoe\nfum\n',
[],
FAIL,
b'Fie\nFoe\nfee\nfum\n',
),
(
b'Fie\nFoe\nfee\nfum\n',
[],
PASS,
b'Fie\nFoe\nfee\nfum\n',
),
(
b'fee\nFie\nFoe\nfum\n',
['--ignore-case'],
PASS,
b'fee\nFie\nFoe\nfum\n',
),
(
b'Fie\nFoe\nfee\nfum\n',
['--ignore-case'],
FAIL,
b'fee\nFie\nFoe\nfum\n',
),
(
b'Fie\nFoe\nfee\nfee\nfum\n',
['--ignore-case'],
FAIL,
b'fee\nfee\nFie\nFoe\nfum\n',
),
(
b'Fie\nFoe\nfee\nfum\n',
['--unique'],
PASS,
b'Fie\nFoe\nfee\nfum\n',
),
(
b'Fie\nFie\nFoe\nfee\nfum\n',
['--unique'],
FAIL,
b'Fie\nFoe\nfee\nfum\n',
),
), ),
) )
def test_integration(input_s, argv, expected_retval, output, tmpdir): def test_integration(input_s, expected_retval, output, tmpdir):
path = tmpdir.join('file.txt') path = tmpdir.join('file.txt')
path.write_binary(input_s) path.write_binary(input_s)
output_retval = main([str(path)] + argv) output_retval = main([path.strpath])
assert path.read_binary() == output assert path.read_binary() == output
assert output_retval == expected_retval assert output_retval == expected_retval
@pytest.mark.parametrize(
('input_s', 'argv'),
(
(
b'fee\nFie\nFoe\nfum\n',
['--unique', '--ignore-case'],
),
(
b'fee\nfee\nFie\nFoe\nfum\n',
['--unique', '--ignore-case'],
),
),
)
def test_integration_invalid_args(input_s, argv, tmpdir):
path = tmpdir.join('file.txt')
path.write_binary(input_s)
with pytest.raises(SystemExit):
main([str(path)] + argv)

View file

@ -1,15 +0,0 @@
from __future__ import annotations
from pre_commit_hooks import fix_byte_order_marker
def test_failure(tmpdir):
f = tmpdir.join('f.txt')
f.write_text('ohai', encoding='utf-8-sig')
assert fix_byte_order_marker.main((str(f),)) == 1
def test_success(tmpdir):
f = tmpdir.join('f.txt')
f.write_text('ohai', encoding='utf-8')
assert fix_byte_order_marker.main((str(f),)) == 0

View file

@ -0,0 +1,165 @@
from __future__ import absolute_import
from __future__ import unicode_literals
import io
import pytest
from pre_commit_hooks.fix_encoding_pragma import _normalize_pragma
from pre_commit_hooks.fix_encoding_pragma import fix_encoding_pragma
from pre_commit_hooks.fix_encoding_pragma import main
def test_integration_inserting_pragma(tmpdir):
path = tmpdir.join('foo.py')
path.write_binary(b'import httplib\n')
assert main((path.strpath,)) == 1
assert path.read_binary() == (
b'# -*- coding: utf-8 -*-\n'
b'import httplib\n'
)
def test_integration_ok(tmpdir):
path = tmpdir.join('foo.py')
path.write_binary(b'# -*- coding: utf-8 -*-\nx = 1\n')
assert main((path.strpath,)) == 0
def test_integration_remove(tmpdir):
path = tmpdir.join('foo.py')
path.write_binary(b'# -*- coding: utf-8 -*-\nx = 1\n')
assert main((path.strpath, '--remove')) == 1
assert path.read_binary() == b'x = 1\n'
def test_integration_remove_ok(tmpdir):
path = tmpdir.join('foo.py')
path.write_binary(b'x = 1\n')
assert main((path.strpath, '--remove')) == 0
@pytest.mark.parametrize(
'input_str',
(
b'',
(
b'# -*- coding: utf-8 -*-\n'
b'x = 1\n'
),
(
b'#!/usr/bin/env python\n'
b'# -*- coding: utf-8 -*-\n'
b'foo = "bar"\n'
),
),
)
def test_ok_inputs(input_str):
bytesio = io.BytesIO(input_str)
assert fix_encoding_pragma(bytesio) == 0
bytesio.seek(0)
assert bytesio.read() == input_str
@pytest.mark.parametrize(
('input_str', 'output'),
(
(
b'import httplib\n',
b'# -*- coding: utf-8 -*-\n'
b'import httplib\n',
),
(
b'#!/usr/bin/env python\n'
b'x = 1\n',
b'#!/usr/bin/env python\n'
b'# -*- coding: utf-8 -*-\n'
b'x = 1\n',
),
(
b'#coding=utf-8\n'
b'x = 1\n',
b'# -*- coding: utf-8 -*-\n'
b'x = 1\n',
),
(
b'#!/usr/bin/env python\n'
b'#coding=utf8\n'
b'x = 1\n',
b'#!/usr/bin/env python\n'
b'# -*- coding: utf-8 -*-\n'
b'x = 1\n',
),
# These should each get truncated
(b'#coding: utf-8\n', b''),
(b'# -*- coding: utf-8 -*-\n', b''),
(b'#!/usr/bin/env python\n', b''),
(b'#!/usr/bin/env python\n#coding: utf8\n', b''),
(b'#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n', b''),
),
)
def test_not_ok_inputs(input_str, output):
bytesio = io.BytesIO(input_str)
assert fix_encoding_pragma(bytesio) == 1
bytesio.seek(0)
assert bytesio.read() == output
def test_ok_input_alternate_pragma():
input_s = b'# coding: utf-8\nx = 1\n'
bytesio = io.BytesIO(input_s)
ret = fix_encoding_pragma(bytesio, expected_pragma=b'# coding: utf-8')
assert ret == 0
bytesio.seek(0)
assert bytesio.read() == input_s
def test_not_ok_input_alternate_pragma():
bytesio = io.BytesIO(b'x = 1\n')
ret = fix_encoding_pragma(bytesio, expected_pragma=b'# coding: utf-8')
assert ret == 1
bytesio.seek(0)
assert bytesio.read() == b'# coding: utf-8\nx = 1\n'
@pytest.mark.parametrize(
('input_s', 'expected'),
(
# Python 2 cli parameters are bytes
(b'# coding: utf-8', b'# coding: utf-8'),
# Python 3 cli parameters are text
('# coding: utf-8', b'# coding: utf-8'),
# trailing whitespace
('# coding: utf-8\n', b'# coding: utf-8'),
),
)
def test_normalize_pragma(input_s, expected):
assert _normalize_pragma(input_s) == expected
def test_integration_alternate_pragma(tmpdir, capsys):
f = tmpdir.join('f.py')
f.write('x = 1\n')
pragma = '# coding: utf-8'
assert main((f.strpath, '--pragma', pragma)) == 1
assert f.read() == '# coding: utf-8\nx = 1\n'
out, _ = capsys.readouterr()
assert out == 'Added `# coding: utf-8` to {}\n'.format(f.strpath)
def test_crlf_ok(tmpdir):
f = tmpdir.join('f.py')
f.write_binary(b'# -*- coding: utf-8 -*-\r\nx = 1\r\n')
assert not main((f.strpath,))
def test_crfl_adds(tmpdir):
f = tmpdir.join('f.py')
f.write_binary(b'x = 1\r\n')
assert main((f.strpath,))
assert f.read_binary() == b'# -*- coding: utf-8 -*-\r\nx = 1\r\n'

View file

@ -1,22 +1,24 @@
from __future__ import annotations from __future__ import absolute_import
import os
import subprocess import subprocess
from unittest import mock
import pytest import pytest
from pre_commit_hooks.forbid_new_submodules import main from pre_commit_hooks.forbid_new_submodules import main
from testing.util import git_commit
@pytest.fixture @pytest.fixture
def git_dir_with_git_dir(tmpdir): def git_dir_with_git_dir(tmpdir):
with tmpdir.as_cwd(): with tmpdir.as_cwd():
subprocess.check_call(('git', 'init', '.')) subprocess.check_call(('git', 'init', '.'))
git_commit('--allow-empty', '-m', 'init') subprocess.check_call((
'git', 'commit', '-m', 'init', '--allow-empty', '--no-gpg-sign',
))
subprocess.check_call(('git', 'init', 'foo')) subprocess.check_call(('git', 'init', 'foo'))
git_commit('--allow-empty', '-m', 'init', cwd=str(tmpdir.join('foo'))) subprocess.check_call(
('git', 'commit', '-m', 'init', '--allow-empty', '--no-gpg-sign'),
cwd=tmpdir.join('foo').strpath,
)
yield yield
@ -31,24 +33,7 @@ def git_dir_with_git_dir(tmpdir):
) )
def test_main_new_submodule(git_dir_with_git_dir, capsys, cmd): def test_main_new_submodule(git_dir_with_git_dir, capsys, cmd):
subprocess.check_call(cmd) subprocess.check_call(cmd)
assert main(('random_non-related_file',)) == 0 assert main() == 1
assert main(('foo',)) == 1
out, _ = capsys.readouterr()
assert out.startswith('foo: new submodule introduced\n')
def test_main_new_submodule_committed(git_dir_with_git_dir, capsys):
rev_parse_cmd = ('git', 'rev-parse', 'HEAD')
from_ref = subprocess.check_output(rev_parse_cmd).decode().strip()
subprocess.check_call(('git', 'submodule', 'add', './foo'))
git_commit('-m', 'new submodule')
to_ref = subprocess.check_output(rev_parse_cmd).decode().strip()
with mock.patch.dict(
os.environ,
{'PRE_COMMIT_FROM_REF': from_ref, 'PRE_COMMIT_TO_REF': to_ref},
):
assert main(('random_non-related_file',)) == 0
assert main(('foo',)) == 1
out, _ = capsys.readouterr() out, _ = capsys.readouterr()
assert out.startswith('foo: new submodule introduced\n') assert out.startswith('foo: new submodule introduced\n')
@ -56,4 +41,4 @@ def test_main_new_submodule_committed(git_dir_with_git_dir, capsys):
def test_main_no_new_submodule(git_dir_with_git_dir): def test_main_no_new_submodule(git_dir_with_git_dir):
open('test.py', 'a+').close() open('test.py', 'a+').close()
subprocess.check_call(('git', 'add', 'test.py')) subprocess.check_call(('git', 'add', 'test.py'))
assert main(('test.py',)) == 0 assert main() == 0

View file

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import absolute_import
from __future__ import unicode_literals
import pytest import pytest
@ -27,7 +28,7 @@ from pre_commit_hooks.mixed_line_ending import main
def test_mixed_line_ending_fixes_auto(input_s, output, tmpdir): def test_mixed_line_ending_fixes_auto(input_s, output, tmpdir):
path = tmpdir.join('file.txt') path = tmpdir.join('file.txt')
path.write_binary(input_s) path.write_binary(input_s)
ret = main((str(path),)) ret = main((path.strpath,))
assert ret == 1 assert ret == 1
assert path.read_binary() == output assert path.read_binary() == output
@ -36,7 +37,7 @@ def test_mixed_line_ending_fixes_auto(input_s, output, tmpdir):
def test_non_mixed_no_newline_end_of_file(tmpdir): def test_non_mixed_no_newline_end_of_file(tmpdir):
path = tmpdir.join('f.txt') path = tmpdir.join('f.txt')
path.write_binary(b'foo\nbar\nbaz') path.write_binary(b'foo\nbar\nbaz')
assert not main((str(path),)) assert not main((path.strpath,))
# the hook *could* fix the end of the file, but leaves it alone # the hook *could* fix the end of the file, but leaves it alone
# this is mostly to document the current behaviour # this is mostly to document the current behaviour
assert path.read_binary() == b'foo\nbar\nbaz' assert path.read_binary() == b'foo\nbar\nbaz'
@ -45,7 +46,7 @@ def test_non_mixed_no_newline_end_of_file(tmpdir):
def test_mixed_no_newline_end_of_file(tmpdir): def test_mixed_no_newline_end_of_file(tmpdir):
path = tmpdir.join('f.txt') path = tmpdir.join('f.txt')
path.write_binary(b'foo\r\nbar\nbaz') path.write_binary(b'foo\r\nbar\nbaz')
assert main((str(path),)) assert main((path.strpath,))
# the hook rewrites the end of the file, this is slightly inconsistent # the hook rewrites the end of the file, this is slightly inconsistent
# with the non-mixed case but I think this is the better behaviour # with the non-mixed case but I think this is the better behaviour
# this is mostly to document the current behaviour # this is mostly to document the current behaviour
@ -68,7 +69,7 @@ def test_mixed_no_newline_end_of_file(tmpdir):
def test_line_endings_ok(fix_option, input_s, tmpdir, capsys): def test_line_endings_ok(fix_option, input_s, tmpdir, capsys):
path = tmpdir.join('input.txt') path = tmpdir.join('input.txt')
path.write_binary(input_s) path.write_binary(input_s)
ret = main((fix_option, str(path))) ret = main((fix_option, path.strpath))
assert ret == 0 assert ret == 0
assert path.read_binary() == input_s assert path.read_binary() == input_s
@ -80,29 +81,29 @@ def test_no_fix_does_not_modify(tmpdir, capsys):
path = tmpdir.join('input.txt') path = tmpdir.join('input.txt')
contents = b'foo\r\nbar\rbaz\nwomp\n' contents = b'foo\r\nbar\rbaz\nwomp\n'
path.write_binary(contents) path.write_binary(contents)
ret = main(('--fix=no', str(path))) ret = main(('--fix=no', path.strpath))
assert ret == 1 assert ret == 1
assert path.read_binary() == contents assert path.read_binary() == contents
out, _ = capsys.readouterr() out, _ = capsys.readouterr()
assert out == f'{path}: mixed line endings\n' assert out == '{}: mixed line endings\n'.format(path)
def test_fix_lf(tmpdir, capsys): def test_fix_lf(tmpdir, capsys):
path = tmpdir.join('input.txt') path = tmpdir.join('input.txt')
path.write_binary(b'foo\r\nbar\rbaz\n') path.write_binary(b'foo\r\nbar\rbaz\n')
ret = main(('--fix=lf', str(path))) ret = main(('--fix=lf', path.strpath))
assert ret == 1 assert ret == 1
assert path.read_binary() == b'foo\nbar\nbaz\n' assert path.read_binary() == b'foo\nbar\nbaz\n'
out, _ = capsys.readouterr() out, _ = capsys.readouterr()
assert out == f'{path}: fixed mixed line endings\n' assert out == '{}: fixed mixed line endings\n'.format(path)
def test_fix_crlf(tmpdir): def test_fix_crlf(tmpdir):
path = tmpdir.join('input.txt') path = tmpdir.join('input.txt')
path.write_binary(b'foo\r\nbar\rbaz\n') path.write_binary(b'foo\r\nbar\rbaz\n')
ret = main(('--fix=crlf', str(path))) ret = main(('--fix=crlf', path.strpath))
assert ret == 1 assert ret == 1
assert path.read_binary() == b'foo\r\nbar\r\nbaz\r\n' assert path.read_binary() == b'foo\r\nbar\r\nbaz\r\n'
@ -112,7 +113,7 @@ def test_fix_lf_all_crlf(tmpdir):
"""Regression test for #239""" """Regression test for #239"""
path = tmpdir.join('input.txt') path = tmpdir.join('input.txt')
path.write_binary(b'foo\r\nbar\r\n') path.write_binary(b'foo\r\nbar\r\n')
ret = main(('--fix=lf', str(path))) ret = main(('--fix=lf', path.strpath))
assert ret == 1 assert ret == 1
assert path.read_binary() == b'foo\nbar\n' assert path.read_binary() == b'foo\nbar\n'

View file

@ -1,23 +1,23 @@
from __future__ import annotations from __future__ import absolute_import
from __future__ import unicode_literals
import pytest import pytest
from pre_commit_hooks.no_commit_to_branch import is_on_branch from pre_commit_hooks.no_commit_to_branch import is_on_branch
from pre_commit_hooks.no_commit_to_branch import main from pre_commit_hooks.no_commit_to_branch import main
from pre_commit_hooks.util import cmd_output from pre_commit_hooks.util import cmd_output
from testing.util import git_commit
def test_other_branch(temp_git_dir): def test_other_branch(temp_git_dir):
with temp_git_dir.as_cwd(): with temp_git_dir.as_cwd():
cmd_output('git', 'checkout', '-b', 'anotherbranch') cmd_output('git', 'checkout', '-b', 'anotherbranch')
assert is_on_branch({'placeholder'}) is False assert is_on_branch({'master'}) is False
def test_multi_branch(temp_git_dir): def test_multi_branch(temp_git_dir):
with temp_git_dir.as_cwd(): with temp_git_dir.as_cwd():
cmd_output('git', 'checkout', '-b', 'another/branch') cmd_output('git', 'checkout', '-b', 'another/branch')
assert is_on_branch({'placeholder'}) is False assert is_on_branch({'master'}) is False
def test_multi_branch_fail(temp_git_dir): def test_multi_branch_fail(temp_git_dir):
@ -26,10 +26,9 @@ def test_multi_branch_fail(temp_git_dir):
assert is_on_branch({'another/branch'}) is True assert is_on_branch({'another/branch'}) is True
def test_exact_branch(temp_git_dir): def test_master_branch(temp_git_dir):
with temp_git_dir.as_cwd(): with temp_git_dir.as_cwd():
cmd_output('git', 'checkout', '-b', 'branchname') assert is_on_branch({'master'}) is True
assert is_on_branch({'branchname'}) is True
def test_main_branch_call(temp_git_dir): def test_main_branch_call(temp_git_dir):
@ -51,11 +50,11 @@ def test_branch_pattern_fail(temp_git_dir):
assert is_on_branch(set(), {'another/.*'}) is True assert is_on_branch(set(), {'another/.*'}) is True
@pytest.mark.parametrize('branch_name', ('somebranch', 'another/branch')) @pytest.mark.parametrize('branch_name', ('master', 'another/branch'))
def test_branch_pattern_multiple_branches_fail(temp_git_dir, branch_name): def test_branch_pattern_multiple_branches_fail(temp_git_dir, branch_name):
with temp_git_dir.as_cwd(): with temp_git_dir.as_cwd():
cmd_output('git', 'checkout', '-b', branch_name) cmd_output('git', 'checkout', '-b', branch_name)
assert main(('--branch', 'somebranch', '--pattern', 'another/.*')) assert main(('--branch', 'master', '--pattern', 'another/.*'))
def test_main_default_call(temp_git_dir): def test_main_default_call(temp_git_dir):
@ -66,15 +65,8 @@ def test_main_default_call(temp_git_dir):
def test_not_on_a_branch(temp_git_dir): def test_not_on_a_branch(temp_git_dir):
with temp_git_dir.as_cwd(): with temp_git_dir.as_cwd():
git_commit('--allow-empty', '-m1') cmd_output('git', 'commit', '--no-gpg-sign', '--allow-empty', '-m1')
head = cmd_output('git', 'rev-parse', 'HEAD').strip() head = cmd_output('git', 'rev-parse', 'HEAD').strip()
cmd_output('git', 'checkout', head) cmd_output('git', 'checkout', head)
# we're not on a branch! # we're not on a branch!
assert main(()) == 0 assert main(()) == 0
@pytest.mark.parametrize('branch_name', ('master', 'main'))
def test_default_branch_names(temp_git_dir, branch_name):
with temp_git_dir.as_cwd():
cmd_output('git', 'checkout', '-b', branch_name)
assert main(()) == 1

View file

@ -1,9 +1,8 @@
from __future__ import annotations
import os import os
import shutil import shutil
import pytest import pytest
from six import PY2
from pre_commit_hooks.pretty_format_json import main from pre_commit_hooks.pretty_format_json import main
from pre_commit_hooks.pretty_format_json import parse_num_to_int from pre_commit_hooks.pretty_format_json import parse_num_to_int
@ -43,6 +42,7 @@ def test_unsorted_main(filename, expected_retval):
assert ret == expected_retval assert ret == expected_retval
@pytest.mark.skipif(PY2, reason='Requires Python3')
@pytest.mark.parametrize( @pytest.mark.parametrize(
('filename', 'expected_retval'), ( ('filename', 'expected_retval'), (
('not_pretty_formatted_json.json', 1), ('not_pretty_formatted_json.json', 1),
@ -52,7 +52,7 @@ def test_unsorted_main(filename, expected_retval):
('tab_pretty_formatted_json.json', 0), ('tab_pretty_formatted_json.json', 0),
), ),
) )
def test_tab_main(filename, expected_retval): def test_tab_main(filename, expected_retval): # pragma: no cover
ret = main(['--indent', '\t', get_resource_path(filename)]) ret = main(['--indent', '\t', get_resource_path(filename)])
assert ret == expected_retval assert ret == expected_retval
@ -69,37 +69,19 @@ def test_autofix_main(tmpdir):
srcfile = tmpdir.join('to_be_json_formatted.json') srcfile = tmpdir.join('to_be_json_formatted.json')
shutil.copyfile( shutil.copyfile(
get_resource_path('not_pretty_formatted_json.json'), get_resource_path('not_pretty_formatted_json.json'),
str(srcfile), srcfile.strpath,
) )
# now launch the autofix on that file # now launch the autofix on that file
ret = main(['--autofix', str(srcfile)]) ret = main(['--autofix', srcfile.strpath])
# it should have formatted it # it should have formatted it
assert ret == 1 assert ret == 1
# file was formatted (shouldn't trigger linter again) # file was formatted (shouldn't trigger linter again)
ret = main([str(srcfile)]) ret = main([srcfile.strpath])
assert ret == 0 assert ret == 0
def test_invalid_main(tmpdir):
srcfile1 = tmpdir.join('not_valid_json.json')
srcfile1.write(
'{\n'
' // not json\n'
' "a": "b"\n'
'}',
)
srcfile2 = tmpdir.join('to_be_json_formatted.json')
srcfile2.write('{ "a": "b" }')
# it should have skipped the first file and formatted the second one
assert main(['--autofix', str(srcfile1), str(srcfile2)]) == 1
# confirm second file was formatted (shouldn't trigger linter again)
assert main([str(srcfile2)]) == 0
def test_orderfile_get_pretty_format(): def test_orderfile_get_pretty_format():
ret = main(( ret = main((
'--top-keys=alist', get_resource_path('pretty_formatted_json.json'), '--top-keys=alist', get_resource_path('pretty_formatted_json.json'),
@ -131,9 +113,9 @@ def test_diffing_output(capsys):
expected_retval = 1 expected_retval = 1
a = os.path.join('a', resource_path) a = os.path.join('a', resource_path)
b = os.path.join('b', resource_path) b = os.path.join('b', resource_path)
expected_out = f'''\ expected_out = '''\
--- {a} --- {}
+++ {b} +++ {}
@@ -1,6 +1,9 @@ @@ -1,6 +1,9 @@
{{ {{
- "foo": - "foo":
@ -148,7 +130,7 @@ def test_diffing_output(capsys):
+ "blah": null, + "blah": null,
+ "foo": "bar" + "foo": "bar"
}} }}
''' '''.format(a, b)
actual_retval = main([resource_path]) actual_retval = main([resource_path])
actual_out, actual_err = capsys.readouterr() actual_out, actual_err = capsys.readouterr()

View file

@ -1,12 +1,15 @@
from __future__ import annotations from __future__ import absolute_import
from __future__ import unicode_literals
import io
from pre_commit_hooks.check_yaml import yaml from pre_commit_hooks.check_yaml import yaml
def test_readme_contains_all_hooks(): def test_readme_contains_all_hooks():
with open('README.md', encoding='UTF-8') as f: with io.open('README.md', encoding='UTF-8') as f:
readme_contents = f.read() readme_contents = f.read()
with open('.pre-commit-hooks.yaml', encoding='UTF-8') as f: with io.open('.pre-commit-hooks.yaml', encoding='UTF-8') as f:
hooks = yaml.load(f) hooks = yaml.load(f)
for hook in hooks: for hook in hooks:
assert f'`{hook["id"]}`' in readme_contents assert '`{}`'.format(hook['id']) in readme_contents

View file

@ -1,19 +0,0 @@
from __future__ import annotations
import pytest
from pre_commit_hooks.removed import main
def test_always_fails():
with pytest.raises(SystemExit) as excinfo:
main((
'autopep8-wrapper', 'autopep8',
'https://github.com/pre-commit/mirrors-autopep8',
'--foo', 'bar',
))
msg, = excinfo.value.args
assert msg == (
'`autopep8-wrapper` has been removed -- '
'use `autopep8` from https://github.com/pre-commit/mirrors-autopep8'
)

View file

@ -1,5 +1,3 @@
from __future__ import annotations
import pytest import pytest
from pre_commit_hooks.requirements_txt_fixer import FAIL from pre_commit_hooks.requirements_txt_fixer import FAIL
@ -32,47 +30,12 @@ from pre_commit_hooks.requirements_txt_fixer import Requirement
), ),
(b'#comment\n\nfoo\nbar\n', FAIL, b'#comment\n\nbar\nfoo\n'), (b'#comment\n\nfoo\nbar\n', FAIL, b'#comment\n\nbar\nfoo\n'),
(b'#comment\n\nbar\nfoo\n', PASS, b'#comment\n\nbar\nfoo\n'), (b'#comment\n\nbar\nfoo\n', PASS, b'#comment\n\nbar\nfoo\n'),
(
b'foo\n\t#comment with indent\nbar\n',
FAIL,
b'\t#comment with indent\nbar\nfoo\n',
),
(
b'bar\n\t#comment with indent\nfoo\n',
PASS,
b'bar\n\t#comment with indent\nfoo\n',
),
(b'\nfoo\nbar\n', FAIL, b'bar\n\nfoo\n'), (b'\nfoo\nbar\n', FAIL, b'bar\n\nfoo\n'),
(b'\nbar\nfoo\n', PASS, b'\nbar\nfoo\n'), (b'\nbar\nfoo\n', PASS, b'\nbar\nfoo\n'),
( (
b'pyramid-foo==1\npyramid>=2\n', b'pyramid==1\npyramid-foo==2\n',
FAIL, PASS,
b'pyramid>=2\npyramid-foo==1\n', b'pyramid==1\npyramid-foo==2\n',
),
(
b'a==1\n'
b'c>=1\n'
b'bbbb!=1\n'
b'c-a>=1;python_version>="3.6"\n'
b'e>=2\n'
b'd>2\n'
b'g<2\n'
b'f<=2\n',
FAIL,
b'a==1\n'
b'bbbb!=1\n'
b'c>=1\n'
b'c-a>=1;python_version>="3.6"\n'
b'd>2\n'
b'e>=2\n'
b'f<=2\n'
b'g<2\n',
),
(b'a==1\nb==1\na==1\n', FAIL, b'a==1\nb==1\n'),
(
b'a==1\nb==1\n#comment about a\na==1\n',
FAIL,
b'#comment about a\na==1\nb==1\n',
), ),
(b'ocflib\nDjango\nPyMySQL\n', FAIL, b'Django\nocflib\nPyMySQL\n'), (b'ocflib\nDjango\nPyMySQL\n', FAIL, b'Django\nocflib\nPyMySQL\n'),
( (
@ -82,38 +45,18 @@ from pre_commit_hooks.requirements_txt_fixer import Requirement
), ),
(b'bar\npkg-resources==0.0.0\nfoo\n', FAIL, b'bar\nfoo\n'), (b'bar\npkg-resources==0.0.0\nfoo\n', FAIL, b'bar\nfoo\n'),
(b'foo\npkg-resources==0.0.0\nbar\n', FAIL, b'bar\nfoo\n'), (b'foo\npkg-resources==0.0.0\nbar\n', FAIL, b'bar\nfoo\n'),
(b'bar\npkg_resources==0.0.0\nfoo\n', FAIL, b'bar\nfoo\n'),
(b'foo\npkg_resources==0.0.0\nbar\n', FAIL, b'bar\nfoo\n'),
( (
b'git+ssh://git_url@tag#egg=ocflib\nDjango\nijk\n', b'git+ssh://git_url@tag#egg=ocflib\nDjango\nijk\n',
FAIL, FAIL,
b'Django\nijk\ngit+ssh://git_url@tag#egg=ocflib\n', b'Django\nijk\ngit+ssh://git_url@tag#egg=ocflib\n',
), ),
(
b'b==1.0.0\n'
b'c=2.0.0 \\\n'
b' --hash=sha256:abcd\n'
b'a=3.0.0 \\\n'
b' --hash=sha256:a1b1c1d1',
FAIL,
b'a=3.0.0 \\\n'
b' --hash=sha256:a1b1c1d1\n'
b'b==1.0.0\n'
b'c=2.0.0 \\\n'
b' --hash=sha256:abcd\n',
),
(
b'a=2.0.0 \\\n --hash=sha256:abcd\nb==1.0.0\n',
PASS,
b'a=2.0.0 \\\n --hash=sha256:abcd\nb==1.0.0\n',
),
), ),
) )
def test_integration(input_s, expected_retval, output, tmpdir): def test_integration(input_s, expected_retval, output, tmpdir):
path = tmpdir.join('file.txt') path = tmpdir.join('file.txt')
path.write_binary(input_s) path.write_binary(input_s)
output_retval = main([str(path)]) output_retval = main([path.strpath])
assert path.read_binary() == output assert path.read_binary() == output
assert output_retval == expected_retval assert output_retval == expected_retval

View file

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import absolute_import
from __future__ import unicode_literals
import os import os
@ -41,14 +42,14 @@ TEST_SORTS = [
@pytest.mark.parametrize('bad_lines,good_lines,retval', TEST_SORTS) @pytest.mark.parametrize('bad_lines,good_lines,retval', TEST_SORTS)
def test_integration_good_bad_lines(tmpdir, bad_lines, good_lines, retval): def test_integration_good_bad_lines(tmpdir, bad_lines, good_lines, retval):
file_path = os.path.join(str(tmpdir), 'foo.yaml') file_path = os.path.join(tmpdir.strpath, 'foo.yaml')
with open(file_path, 'w') as f: with open(file_path, 'w') as f:
f.write('\n'.join(bad_lines) + '\n') f.write('\n'.join(bad_lines) + '\n')
assert main([file_path]) == retval assert main([file_path]) == retval
with open(file_path) as f: with open(file_path, 'r') as f:
assert [line.rstrip() for line in f.readlines()] == good_lines assert [line.rstrip() for line in f.readlines()] == good_lines

View file

@ -1,4 +1,6 @@
from __future__ import annotations from __future__ import absolute_import
from __future__ import print_function
from __future__ import unicode_literals
import textwrap import textwrap
@ -37,12 +39,6 @@ TESTS = (
1, 1,
), ),
('"foo""bar"', "'foo''bar'", 1), ('"foo""bar"', "'foo''bar'", 1),
pytest.param(
"f'hello{\"world\"}'",
"f'hello{\"world\"}'",
0,
id='ignore nested fstrings',
),
) )
@ -50,7 +46,7 @@ TESTS = (
def test_rewrite(input_s, output, expected_retval, tmpdir): def test_rewrite(input_s, output, expected_retval, tmpdir):
path = tmpdir.join('file.py') path = tmpdir.join('file.py')
path.write(input_s) path.write(input_s)
retval = main([str(path)]) retval = main([path.strpath])
assert path.read() == output assert path.read() == output
assert retval == expected_retval assert retval == expected_retval
@ -58,5 +54,5 @@ def test_rewrite(input_s, output, expected_retval, tmpdir):
def test_rewrite_crlf(tmpdir): def test_rewrite_crlf(tmpdir):
f = tmpdir.join('f.py') f = tmpdir.join('f.py')
f.write_binary(b'"foo"\r\n"bar"\r\n') f.write_binary(b'"foo"\r\n"bar"\r\n')
assert main((str(f),)) assert main((f.strpath,))
assert f.read_binary() == b"'foo'\r\n'bar'\r\n" assert f.read_binary() == b"'foo'\r\n'bar'\r\n"

View file

@ -1,5 +1,3 @@
from __future__ import annotations
from pre_commit_hooks.tests_should_end_in_test import main from pre_commit_hooks.tests_should_end_in_test import main
@ -43,8 +41,3 @@ def test_main_not_django_fails():
def test_main_django_fails(): def test_main_django_fails():
ret = main(['--django', 'foo_test.py', 'test_bar.py', 'test_baz.py']) ret = main(['--django', 'foo_test.py', 'test_bar.py', 'test_baz.py'])
assert ret == 1 assert ret == 1
def test_main_pytest_test_first():
assert main(['--pytest-test-first', 'test_foo.py']) == 0
assert main(['--pytest-test-first', 'foo_test.py']) == 1

View file

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import absolute_import
from __future__ import unicode_literals
import pytest import pytest
@ -15,14 +16,14 @@ from pre_commit_hooks.trailing_whitespace_fixer import main
def test_fixes_trailing_whitespace(input_s, expected, tmpdir): def test_fixes_trailing_whitespace(input_s, expected, tmpdir):
path = tmpdir.join('file.md') path = tmpdir.join('file.md')
path.write(input_s) path.write(input_s)
assert main((str(path),)) == 1 assert main((path.strpath,)) == 1
assert path.read() == expected assert path.read() == expected
def test_ok_no_newline_end_of_file(tmpdir): def test_ok_no_newline_end_of_file(tmpdir):
filename = tmpdir.join('f') filename = tmpdir.join('f')
filename.write_binary(b'foo\nbar') filename.write_binary(b'foo\nbar')
ret = main((str(filename),)) ret = main((filename.strpath,))
assert filename.read_binary() == b'foo\nbar' assert filename.read_binary() == b'foo\nbar'
assert ret == 0 assert ret == 0
@ -30,7 +31,7 @@ def test_ok_no_newline_end_of_file(tmpdir):
def test_ok_with_dos_line_endings(tmpdir): def test_ok_with_dos_line_endings(tmpdir):
filename = tmpdir.join('f') filename = tmpdir.join('f')
filename.write_binary(b'foo\r\nbar\r\nbaz\r\n') filename.write_binary(b'foo\r\nbar\r\nbaz\r\n')
ret = main((str(filename),)) ret = main((filename.strpath,))
assert filename.read_binary() == b'foo\r\nbar\r\nbaz\r\n' assert filename.read_binary() == b'foo\r\nbar\r\nbaz\r\n'
assert ret == 0 assert ret == 0
@ -45,7 +46,7 @@ def test_fixes_markdown_files(tmpdir, ext):
'\t\n' # trailing tabs are stripped anyway '\t\n' # trailing tabs are stripped anyway
'\n ', # whitespace at the end of the file is removed '\n ', # whitespace at the end of the file is removed
) )
ret = main((str(path), f'--markdown-linebreak-ext={ext}')) ret = main((path.strpath, '--markdown-linebreak-ext={}'.format(ext)))
assert ret == 1 assert ret == 1
assert path.read() == ( assert path.read() == (
'foo \n' 'foo \n'
@ -65,7 +66,7 @@ def test_markdown_linebreak_ext_badopt(arg):
def test_prints_warning_with_no_markdown_ext(capsys, tmpdir): def test_prints_warning_with_no_markdown_ext(capsys, tmpdir):
f = tmpdir.join('f').ensure() f = tmpdir.join('f').ensure()
assert main((str(f), '--no-markdown-linebreak-ext')) == 0 assert main((f.strpath, '--no-markdown-linebreak-ext')) == 0
out, _ = capsys.readouterr() out, _ = capsys.readouterr()
assert out == '--no-markdown-linebreak-ext now does nothing!\n' assert out == '--no-markdown-linebreak-ext now does nothing!\n'
@ -74,7 +75,7 @@ def test_preserve_non_utf8_file(tmpdir):
non_utf8_bytes_content = b'<a>\xe9 \n</a>\n' non_utf8_bytes_content = b'<a>\xe9 \n</a>\n'
path = tmpdir.join('file.txt') path = tmpdir.join('file.txt')
path.write_binary(non_utf8_bytes_content) path.write_binary(non_utf8_bytes_content)
ret = main([str(path)]) ret = main([path.strpath])
assert ret == 1 assert ret == 1
assert path.size() == (len(non_utf8_bytes_content) - 1) assert path.size() == (len(non_utf8_bytes_content) - 1)
@ -83,7 +84,7 @@ def test_custom_charset_change(tmpdir):
# strip spaces only, no tabs # strip spaces only, no tabs
path = tmpdir.join('file.txt') path = tmpdir.join('file.txt')
path.write('\ta \t \n') path.write('\ta \t \n')
ret = main([str(path), '--chars', ' ']) ret = main([path.strpath, '--chars', ' '])
assert ret == 1 assert ret == 1
assert path.read() == '\ta \t\n' assert path.read() == '\ta \t\n'
@ -91,13 +92,13 @@ def test_custom_charset_change(tmpdir):
def test_custom_charset_no_change(tmpdir): def test_custom_charset_no_change(tmpdir):
path = tmpdir.join('file.txt') path = tmpdir.join('file.txt')
path.write('\ta \t\n') path.write('\ta \t\n')
ret = main([str(path), '--chars', ' ']) ret = main([path.strpath, '--chars', ' '])
assert ret == 0 assert ret == 0
def test_markdown_with_custom_charset(tmpdir): def test_markdown_with_custom_charset(tmpdir):
path = tmpdir.join('file.md') path = tmpdir.join('file.md')
path.write('\ta \t \n') path.write('\ta \t \n')
ret = main([str(path), '--chars', ' ', '--markdown-linebreak-ext', '*']) ret = main([path.strpath, '--chars', ' ', '--markdown-linebreak-ext', '*'])
assert ret == 1 assert ret == 1
assert path.read() == '\ta \t \n' assert path.read() == '\ta \t \n'

View file

@ -1,10 +1,10 @@
from __future__ import annotations from __future__ import absolute_import
from __future__ import unicode_literals
import pytest import pytest
from pre_commit_hooks.util import CalledProcessError from pre_commit_hooks.util import CalledProcessError
from pre_commit_hooks.util import cmd_output from pre_commit_hooks.util import cmd_output
from pre_commit_hooks.util import zsplit
def test_raises_on_error(): def test_raises_on_error():
@ -15,13 +15,3 @@ def test_raises_on_error():
def test_output(): def test_output():
ret = cmd_output('sh', '-c', 'echo hi') ret = cmd_output('sh', '-c', 'echo hi')
assert ret == 'hi\n' assert ret == 'hi\n'
@pytest.mark.parametrize('out', ('\0f1\0f2\0', '\0f1\0f2', 'f1\0f2\0'))
def test_check_zsplits_str_correctly(out):
assert zsplit(out) == ['f1', 'f2']
@pytest.mark.parametrize('out', ('\0\0', '\0', ''))
def test_check_zsplit_returns_empty(out):
assert zsplit(out) == []

View file

@ -1,5 +1,5 @@
[tox] [tox]
envlist = py,pre-commit envlist = py27,py36,py37,pypy,pypy3,pre-commit
[testenv] [testenv]
deps = -rrequirements-dev.txt deps = -rrequirements-dev.txt
@ -11,7 +11,8 @@ setenv =
commands = commands =
coverage erase coverage erase
coverage run -m pytest {posargs:tests} coverage run -m pytest {posargs:tests}
coverage report coverage report --fail-under 100
pre-commit install
[testenv:pre-commit] [testenv:pre-commit]
skip_install = true skip_install = true