diff --git a/.coveragerc b/.coveragerc index 714aaa1..0a55c5f 100644 --- a/.coveragerc +++ b/.coveragerc @@ -5,11 +5,12 @@ source = omit = .tox/* /usr/* - */tmp* setup.py get-git-lfs.py [report] +show_missing = True +skip_covered = True exclude_lines = # Have to re-enable the standard pragma \#\s*pragma: no cover diff --git a/.gitignore b/.gitignore index 2626934..c00e966 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ *.iml *.py[co] .*.sw[a-z] +.pytest_cache .coverage .idea .project @@ -11,3 +12,5 @@ /venv* coverage-html dist +# SublimeText project/workspace files +*.sublime-* diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a3bb7a4..e657ccb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,5 +1,6 @@ +repos: - repo: https://github.com/pre-commit/pre-commit-hooks - sha: v0.7.0 + rev: v1.3.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer @@ -13,12 +14,19 @@ - id: requirements-txt-fixer - id: flake8 - repo: https://github.com/pre-commit/pre-commit - sha: v0.12.2 + rev: v1.7.0 hooks: - - id: validate_config - id: validate_manifest - repo: https://github.com/asottile/reorder_python_imports - sha: v0.3.1 + rev: v1.0.1 hooks: - id: reorder-python-imports language_version: python2.7 +- repo: https://github.com/asottile/pyupgrade + rev: v1.2.0 + hooks: + - id: pyupgrade +- repo: https://github.com/asottile/add-trailing-comma + rev: v0.6.4 + hooks: + - id: add-trailing-comma diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml index 0604585..1f62b3d 100644 --- a/.pre-commit-hooks.yaml +++ b/.pre-commit-hooks.yaml @@ -3,147 +3,263 @@ description: "Runs autopep8 over python source. If you configure additional arguments you'll want to at least include -i." entry: autopep8-wrapper language: python - files: \.py$ + types: [python] args: [-i] + # for backward compatibility + files: '' + minimum_pre_commit_version: 0.15.0 - id: check-added-large-files name: Check for added large files description: Prevent giant files from being committed entry: check-added-large-files language: python - # Match all files + # for backward compatibility files: '' + minimum_pre_commit_version: 0.15.0 - id: check-ast name: Check python ast description: Simply check whether the files parse as valid python. entry: check-ast language: python - files: '\.py$' + types: [python] + # for backward compatibility + files: '' + minimum_pre_commit_version: 0.15.0 - id: check-byte-order-marker name: Check for byte-order marker description: Forbid files which have a UTF-8 byte-order marker entry: check-byte-order-marker language: python - files: '\.py$' + types: [python] + # for backward compatibility + files: '' + minimum_pre_commit_version: 0.15.0 +- id: check-builtin-literals + name: Check builtin type constructor use + description: Require literal syntax when initializing empty or zero Python builtin types. + entry: check-builtin-literals + language: python + types: [python] + # for backward compatibility + files: '' + minimum_pre_commit_version: 0.15.0 - id: check-case-conflict name: Check for case conflicts description: Check for files that would conflict in case-insensitive filesystems entry: check-case-conflict language: python - # Match all files + # for backward compatibility files: '' + minimum_pre_commit_version: 0.15.0 - id: check-docstring-first name: Check docstring is first description: Checks a common error of defining a docstring after code. entry: check-docstring-first language: python - files: \.py$ + types: [python] + # for backward compatibility + files: '' + minimum_pre_commit_version: 0.15.0 +- id: check-executables-have-shebangs + name: Check that executables have shebangs + description: Ensures that (non-binary) executables have a shebang. + entry: check-executables-have-shebangs + language: python + types: [text, executable] + # for backward compatibility + files: '' + minimum_pre_commit_version: 0.15.0 - id: check-json name: Check JSON description: This hook checks json files for parseable syntax. entry: check-json language: python - files: \.json$ + types: [json] + # for backward compatibility + files: '' + minimum_pre_commit_version: 0.15.0 - id: pretty-format-json name: Pretty format JSON description: This hook sets a standard for formatting JSON files. entry: pretty-format-json language: python - files: \.json$ + types: [json] + # for backward compatibility + files: '' + minimum_pre_commit_version: 0.15.0 - id: check-merge-conflict name: Check for merge conflicts description: Check for files that contain merge conflict strings. entry: check-merge-conflict language: python - # Match all files + types: [text] + # for backward compatibility files: '' + minimum_pre_commit_version: 0.15.0 - id: check-symlinks name: Check for broken symlinks description: Checks for symlinks which do not point to anything. entry: check-symlinks language: python - # Match all files + types: [symlink] + # for backward compatibility files: '' + minimum_pre_commit_version: 0.15.0 +- id: check-vcs-permalinks + name: Check vcs permalinks + description: Ensures that links to vcs websites are permalinks. + entry: check-vcs-permalinks + language: python + types: [text] + # for backward compatibility + files: '' + minimum_pre_commit_version: 0.15.0 - id: check-xml name: Check Xml description: This hook checks xml files for parseable syntax. entry: check-xml language: python - files: \.xml$ + types: [xml] + # for backward compatibility + files: '' + minimum_pre_commit_version: 0.15.0 - id: check-yaml name: Check Yaml description: This hook checks yaml files for parseable syntax. entry: check-yaml language: python - files: \.(yaml|yml|eyaml)$ + types: [yaml] + # for backward compatibility + files: '' + minimum_pre_commit_version: 0.15.0 - id: debug-statements name: Debug Statements (Python) - description: This hook checks that debug statements (pdb, ipdb, pudb) are not imported on commit. + description: Check for debugger imports and py37+ `breakpoint()` calls in python source. entry: debug-statement-hook language: python - files: \.py$ + types: [python] + # for backward compatibility + files: '' + minimum_pre_commit_version: 0.15.0 - id: detect-aws-credentials name: Detect AWS Credentials description: Detects *your* aws credentials from the aws cli credentials file entry: detect-aws-credentials language: python + types: [text] + # for backward compatibility files: '' + minimum_pre_commit_version: 0.15.0 - id: detect-private-key name: Detect Private Key description: Detects the presence of private keys entry: detect-private-key language: python + types: [text] + # for backward compatibility files: '' + minimum_pre_commit_version: 0.15.0 - id: double-quote-string-fixer name: Fix double quoted strings description: This hook replaces double quoted strings with single quoted strings entry: double-quote-string-fixer language: python - files: \.py$ + types: [python] + # for backward compatibility + files: '' + minimum_pre_commit_version: 0.15.0 - id: end-of-file-fixer name: Fix End of Files description: Ensures that a file is either empty, or ends with one newline. entry: end-of-file-fixer language: python - files: \.(asciidoc|adoc|coffee|cpp|css|c|ejs|erb|groovy|h|haml|hh|hpp|hxx|html|in|j2|jade|json|js|less|markdown|md|ml|mli|pp|py|rb|rs|R|scala|scss|sh|slim|tex|tmpl|ts|txt|yaml|yml)$ + types: [text] + # for backward compatibility + files: '' + minimum_pre_commit_version: 0.15.0 +- id: file-contents-sorter + name: File Contents Sorter + 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 + language: python + files: '^$' - id: fix-encoding-pragma name: Fix python encoding pragma language: python entry: fix-encoding-pragma description: 'Add # -*- coding: utf-8 -*- to the top of python files' - files: \.py$ + types: [python] + # for backward compatibility + files: '' + minimum_pre_commit_version: 0.15.0 - id: flake8 name: Flake8 description: This hook runs flake8. entry: flake8 language: python - files: \.py$ + types: [python] + # for backward compatibility + files: '' + minimum_pre_commit_version: 0.15.0 - id: forbid-new-submodules name: Forbid new submodules language: python entry: forbid-new-submodules description: Prevent addition of new git submodules + # for backward compatibility files: '' + minimum_pre_commit_version: 0.15.0 +- id: mixed-line-ending + name: Mixed line ending + description: Replaces or checks mixed line ending + entry: mixed-line-ending + language: python + types: [text] + # for backward compatibility + files: '' + minimum_pre_commit_version: 0.15.0 - id: name-tests-test name: Tests should end in _test.py description: This verifies that test files are named correctly entry: name-tests-test language: python - files: tests/.+\.py$ + files: (^|/)tests/.+\.py$ +- id: no-commit-to-branch + name: "Don't commit to branch" + entry: no-commit-to-branch + language: python + pass_filenames: false + always_run: true + # for backward compatibility + files: '' + minimum_pre_commit_version: 0.15.0 - id: pyflakes name: Pyflakes (DEPRECATED, use flake8) description: This hook runs pyflakes. (This is deprecated, use flake8). entry: pyflakes language: python - files: \.py$ + types: [python] + # for backward compatibility + files: '' + minimum_pre_commit_version: 0.15.0 - id: requirements-txt-fixer name: Fix requirements.txt description: Sorts entries in requirements.txt entry: requirements-txt-fixer language: python files: requirements.*\.txt$ +- id: sort-simple-yaml + name: Sort simple YAML files + language: python + entry: sort-simple-yaml + description: Sorts simple YAML files which consist only of top-level keys, preserving comments and blocks. + files: '^$' - id: trailing-whitespace name: Trim Trailing Whitespace description: This hook trims trailing whitespace. entry: trailing-whitespace-fixer language: python - files: \.(asciidoc|adoc|coffee|cpp|css|c|ejs|erb|groovy|h|haml|hh|hpp|hxx|html|in|j2|jade|json|js|less|markdown|md|ml|mli|pp|py|rb|rs|R|scala|scss|sh|slim|tex|tmpl|ts|txt|yaml|yml)$ + types: [text] + # for backward compatibility + files: '' + minimum_pre_commit_version: 0.15.0 diff --git a/.travis.yml b/.travis.yml index 0bfeae6..6d428ce 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,8 +8,8 @@ matrix: - env: TOXENV=py36 python: 3.6 - env: TOXENV=pypy -#install: pip install coveralls tox -install: pip install tox + python: pypy-5.7.1 +install: pip install coveralls tox script: tox before_install: # Install git-lfs for a test @@ -18,5 +18,5 @@ after_success: coveralls cache: directories: - $HOME/.cache/pip - - $HOME/.pre-commit + - $HOME/.cache/pre-commit - /tmp/git-lfs diff --git a/CHANGELOG b/CHANGELOG deleted file mode 100644 index c489206..0000000 --- a/CHANGELOG +++ /dev/null @@ -1,86 +0,0 @@ -0.7.1 -===== -- Don't false positive on files where trailing whitespace isn't changed. - -0.7.0 -===== -- Improve search for detecting aws keys -- Add .pre-commit-hooks.yaml for forward compatibility - -0.6.1 -===== -- trailing-whitespace-hook: restore original file on catastrophic failure -- trailing-whitespace-hook: support crlf -- check-yaml: Use safe_load -- check-json: allow custom key sort -- check-json: display filename for non-utf8 files -- New hook: forbid-new-submodules - -0.6.0 -===== -- Merge conflict detection no longer crashes on binary files -- Indentation in json may be an arbitrary separator -- Editable requirements are properly sorted -- Encoding pragma fixer pragma is configurable - -0.5.1 -===== -- Add a --no-sort-keys to json pretty formatter -- Add a --remove to fix-encoding-pragma - -0.5.0 -===== -- Add check-byte-order-marker -- Add check-synlinks -- check-large-files-added understands git-lfs -- Support older git -- Fix regex for --django in test name checker -- Add fix-encoding-pragma hook -- requirements-txt-fixer now sorts like latest pip -- Add check-ast hook -- Add detect-aws-credentials hook -- Allow binary files to pass private key hook -- Add pretty-format-json hook - -0.4.2 -===== -- Add --django to test name checker -- Add check-merge-conflict hook -- Remove dependency on plumbum -- Add q as a debug statement -- Don't detect markup titles as conflicts -- Teach trailing-whitespace about markdown -- Quickfix for pyflakes - flake8 version conflict - -0.4.1 -===== -- Respect configuration when running autopep8 -- Quickfix for pep8 version conflicts - -0.4.0 -===== -- Fix trailing-whitespace on OS X -- Add check-added-large-files hook -- Add check-docstring-first hook -- Add requirements-txt-fixer hook -- Add check-case-conflict hook -- Use yaml's CLoader when available in check-yaml for more speed -- Add check-xml hook -- Fix end-of-file-fixer for windows -- Add double-quote-string-fixer hook - -0.3.0 -===== -- Add autopep8-wrapper hook - -0.2.0 -===== -- Add check-json hook - -0.1.1 -===== -- Don't crash on non-parseable files for debug-statement-hook - -0.1.0 -===== -- Initial Release diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..b58ddbc --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,223 @@ +1.3.0 +===== + +### Features +- Add an `--unsafe` argument to `check-yaml` to allow custom yaml tags + - #273 issue by @blackillzone. + - #274 PR by @asottile. +- Automatically remove `pkg-resources==0.0.0` in `requirements-txt-fixer` + - #275 PR by @nvtkaszpir. +- Detect `breakpoint()` (python3.7+) in `debug-statements` hook. + - #283 PR by @asottile. +- Detect sshcom and putty hooks in `detect-private-key` + - #287 PR by @vin01. + +### Fixes +- Open files as UTF-8 (`autopep8-wrapper`, `check-docstring-first`, + `double-quote-string-fixer`) + - #279 PR by @nvtkaszpir. +- Fix `AttributeError` in `check-builtin-literals` for some functions + - #285 issue by @EgoWumpus. + - #286 PR by @asottile. + +1.2.3 +===== + +### Fixes +- `trailing-whitespace` entrypoint was incorrect. + - f6780b9 by @asottile. + +1.2.2 +===== + +### Fixes +- `trailing-whitespace` no longer adds a missing newline at end-of-file + - #270 issue by @fractos. + - #271 PR by @asottile. + +1.2.1-1 +======= + +(Note: this is a tag-only release as no code changes occurred) + +### Fixes: +- Don't pass filenames for `no-commit-to-branch` + - #268 issue by @dongyuzheng. + - #269 PR by @asottile. + +1.2.1 +===== +### Fixes: +- `detect-aws-credentials` false positive when key was empty + - #258 issue by @PVSec. + - #260 PR by @PVSec. +- `no-commit-to-branch` no longer crashes when not on a branch + - #265 issue by @hectorv. + - #266 PR by @asottile. + +1.2.0 +===== +### Features: +- Add new `check-builtin-literals` hook. + - #249 #251 PR by @benwebber. +- `pretty-format-json` no longer depends on `simplejson`. + - #254 PR by @cas--. +- `detect-private-key` now detects gcp keys. + - #255 issue by @SaMnCo @nicain. + - #256 PR by @nicain. + +1.1.1 +===== +### Fixes: +- Fix output interleaving in `check-vcs-permalinks` under python3. + - #245 PR by @asottile. + +1.1.0 +===== +### Features: +- `check-yaml` gains a `--allow-multiple-documents` (`-m`) argument to allow + linting of files using the + [multi document syntax](http://www.yaml.org/spec/1.2/spec.html#YAML) + - pre-commit/pre-commit#635 issue by @geekobi. + - #244 PR by @asottile. + +1.0.0 +===== +### Features: +- New hook: `check-vcs-permalinks` for ensuring permalinked github urls. + - #241 PR by @asottile. + +### Fixes: +- Fix `trailing-whitespace` for non-utf8 files on macos + - #242 PR by @asottile. +- Fix `requirements-txt-fixer` for files ending in comments + - #243 PR by @asottile. + +0.9.5 +===== +- Fix mixed-line-endings `--fix=...` when whole file is a different ending + +0.9.4 +===== +- Fix entry point for `mixed-line-ending` + +0.9.3 +===== +- New hook: `mixed-line-ending` + +0.9.2 +===== +- Report full python version in `check-ast`. +- Apply a more strict regular expression for `name-tests-test` +- Upgrade binding for `git-lfs` for `check-added-large-files`. The oldest + version that is supported is 2.2.1 (2.2.0 will incorrectly refer to all + files as "lfs" (false negative) and earlier versions will crash. +- `debug-statements` now works for non-utf-8 files. + +0.9.1 +===== +- Add `check-executables-have-shebangs` hook. + +0.9.0 +===== +- Add `sort-simple-yaml` hook +- Fix `requirements-txt-fixer` for empty files +- Add `file-contents-sorter` hook for sorting flat files +- `check-merge-conflict` now recognizes rebase conflicts +- Metadata now uses `types` (and therefore requires pre-commit 0.15.0). This + allows the text processing hooks to match *all* text files (and to match + files which would only be classifiable by their shebangs). + +0.8.0 +===== +- Add flag allowing missing keys to `detect-aws-credentials` +- Handle django default `tests.py` in `name-tests-test` +- Add `--no-ensure-ascii` option to `pretty-format-json` +- Add `no-commit-to-branch` hook + +0.7.1 +===== +- Don't false positive on files where trailing whitespace isn't changed. + +0.7.0 +===== +- Improve search for detecting aws keys +- Add .pre-commit-hooks.yaml for forward compatibility + +0.6.1 +===== +- trailing-whitespace-hook: restore original file on catastrophic failure +- trailing-whitespace-hook: support crlf +- check-yaml: Use safe_load +- check-json: allow custom key sort +- check-json: display filename for non-utf8 files +- New hook: forbid-new-submodules + +0.6.0 +===== +- Merge conflict detection no longer crashes on binary files +- Indentation in json may be an arbitrary separator +- Editable requirements are properly sorted +- Encoding pragma fixer pragma is configurable + +0.5.1 +===== +- Add a --no-sort-keys to json pretty formatter +- Add a --remove to fix-encoding-pragma + +0.5.0 +===== +- Add check-byte-order-marker +- Add check-synlinks +- check-large-files-added understands git-lfs +- Support older git +- Fix regex for --django in test name checker +- Add fix-encoding-pragma hook +- requirements-txt-fixer now sorts like latest pip +- Add check-ast hook +- Add detect-aws-credentials hook +- Allow binary files to pass private key hook +- Add pretty-format-json hook + +0.4.2 +===== +- Add --django to test name checker +- Add check-merge-conflict hook +- Remove dependency on plumbum +- Add q as a debug statement +- Don't detect markup titles as conflicts +- Teach trailing-whitespace about markdown +- Quickfix for pyflakes - flake8 version conflict + +0.4.1 +===== +- Respect configuration when running autopep8 +- Quickfix for pep8 version conflicts + +0.4.0 +===== +- Fix trailing-whitespace on OS X +- Add check-added-large-files hook +- Add check-docstring-first hook +- Add requirements-txt-fixer hook +- Add check-case-conflict hook +- Use yaml's CLoader when available in check-yaml for more speed +- Add check-xml hook +- Fix end-of-file-fixer for windows +- Add double-quote-string-fixer hook + +0.3.0 +===== +- Add autopep8-wrapper hook + +0.2.0 +===== +- Add check-json hook + +0.1.1 +===== +- Don't crash on non-parseable files for debug-statement-hook + +0.1.0 +===== +- Initial Release diff --git a/Makefile b/Makefile deleted file mode 100644 index b38b1ae..0000000 --- a/Makefile +++ /dev/null @@ -1,24 +0,0 @@ -REBUILD_FLAG = - -.PHONY: all -all: venv test - -.PHONY: venv -venv: .venv.touch - tox -e venv $(REBUILD_FLAG) - -.PHONY: tests test -tests: test -test: .venv.touch - tox $(REBUILD_FLAG) - -.venv.touch: setup.py requirements-dev.txt - $(eval REBUILD_FLAG := --recreate) - touch .venv.touch - -.PHONY: clean -clean: - find . -name '*.pyc' -delete - rm -rf .tox - rm -rf ./venv-* - rm -f .venv.touch diff --git a/README.md b/README.md index ea35111..b805949 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,8 @@ See also: https://github.com/pre-commit/pre-commit Add this to your `.pre-commit-config.yaml` - - repo: git://github.com/pre-commit/pre-commit-hooks - sha: v0.7.1 # Use the ref you want to point at + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v1.3.0 # Use the ref you want to point at hooks: - id: trailing-whitespace # - id: ... @@ -25,21 +25,40 @@ Add this to your `.pre-commit-config.yaml` - `autopep8-wrapper` - Runs autopep8 over python source. - Ignore PEP 8 violation types with `args: ['-i', '--ignore=E000,...']` or - through configuration of the `[pep8]` section in setup.cfg / tox.ini. + through configuration of the `[pycodestyle]` section in + setup.cfg / tox.ini. - `check-added-large-files` - Prevent giant files from being committed. - Specify what is "too large" with `args: ['--maxkb=123']` (default=500kB). + - If `git-lfs` is installed, lfs files will be skipped + (requires `git-lfs>=2.2.1`) - `check-ast` - Simply check whether files parse as valid python. +- `check-builtin-literals` - Require literal syntax when initializing empty or zero Python builtin types. + - Allows calling constructors with positional arguments (e.g., `list('abc')`). + - Allows calling constructors from the `builtins` (`__builtin__`) namespace (`builtins.list()`). + - Ignore this requirement for specific builtin types with `--ignore=type1,type2,…`. + - 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 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` - Checks that non-binary executables have a + proper shebang. - `check-json` - Attempts to load all json files to verify syntax. - `check-merge-conflict` - Check for files that contain merge conflict strings. - `check-symlinks` - Checks for symlinks which do not point to anything. +- `check-vcs-permalinks` - Ensures that links to vcs websites are permalinks. - `check-xml` - Attempts to load all xml files to verify syntax. - `check-yaml` - Attempts to load all yaml files to verify syntax. -- `debug-statements` - Check for pdb / ipdb / pudb statements in code. + - `--allow-multiple-documents` - allow yaml files which use the + [multi-document syntax](http://www.yaml.org/spec/1.2/spec.html#YAML) + - `--unsafe` - Instead of loading the files, simply parse them for syntax. + A syntax-only check enables extensions and unsafe constructs which would + otherwise be forbidden. Using this option removes all guarantees of + portability to other yaml implementations. + Implies `--allow-multiple-documents`. +- `debug-statements` - Check for debugger imports and py37+ `breakpoint()` + calls in python source. - `detect-aws-credentials` - Checks for the existence of AWS secrets that you have set up with the AWS CLI. The following arguments are available: @@ -52,10 +71,21 @@ Add this to your `.pre-commit-config.yaml` - `end-of-file-fixer` - 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` - Sort the lines in specified files (defaults to alphabetical). 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. - `flake8` - Run flake8 on your python files. - `forbid-new-submodules` - Prevent addition of new git submodules. +- `mixed-line-ending` - Replaces or checks mixed line ending. + - `--fix={auto,crlf,lf,no}` + - `auto` - Replaces automatically the most frequent line ending. This is the default argument. + - `crlf`, `lf` - Forces to replace line ending by respectively CRLF and LF. + - `no` - Checks if there is any mixed line ending without modifying any file. - `name-tests-test` - Assert that files in tests/ end in `_test.py`. - Use `args: ['--django']` to match `test*.py` instead. +- `no-commit-to-branch` - Protect specific branches from direct checkins. + - Use `args: [--branch ]` to set the branch. `master` is the + default if no argument is set. + - `-b` / `--branch` may be specified multiple times to protect multiple + branches. - `pyflakes` - Run pyflakes on your python files. - `pretty-format-json` - Checks that all your JSON files are pretty. "Pretty" here means that keys are sorted and indented. You can configure this with @@ -64,7 +94,8 @@ Add this to your `.pre-commit-config.yaml` - `--indent ...` - Control the indentation (either a number for a number of spaces or a string of whitespace). Defaults to 4 spaces. - `--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. -- `requirements-txt-fixer` - Sorts entries in requirements.txt +- `requirements-txt-fixer` - Sorts entries in requirements.txt and removes incorrect entry for `pkg-resources==0.0.0` +- `sort-simple-yaml` - Sorts simple YAML files which consist only of top-level keys, preserving comments and blocks. - `trailing-whitespace` - Trims trailing whitespace. - Markdown linebreak trailing spaces preserved for `.md` and`.markdown`; use `args: ['--markdown-linebreak-ext=txt,text']` to add other extensions, diff --git a/appveyor.yml b/appveyor.yml index a0df40b..8df64a5 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -14,4 +14,4 @@ test_script: tox cache: - '%LOCALAPPDATA%\pip\cache' - - '%USERPROFILE%\.pre-commit' + - '%USERPROFILE%\.cache\pre-commit' diff --git a/get-git-lfs.py b/get-git-lfs.py index c1f2197..48dd31e 100755 --- a/get-git-lfs.py +++ b/get-git-lfs.py @@ -8,9 +8,9 @@ from urllib.request import urlopen DOWNLOAD_PATH = ( 'https://github.com/github/git-lfs/releases/download/' - 'v1.1.0/git-lfs-linux-amd64-1.1.0.tar.gz' + 'v2.2.1/git-lfs-linux-amd64-2.2.1.tar.gz' ) -PATH_IN_TAR = 'git-lfs-1.1.0/git-lfs' +PATH_IN_TAR = 'git-lfs-2.2.1/git-lfs' DEST_PATH = '/tmp/git-lfs/git-lfs' DEST_DIR = os.path.dirname(DEST_PATH) diff --git a/hooks.yaml b/hooks.yaml index 0604585..4552fa7 100644 --- a/hooks.yaml +++ b/hooks.yaml @@ -1,149 +1,186 @@ - id: autopep8-wrapper - name: autopep8 wrapper - description: "Runs autopep8 over python source. If you configure additional arguments you'll want to at least include -i." - entry: autopep8-wrapper - language: python - files: \.py$ - args: [-i] + language: system + name: upgrade-your-pre-commit-version + entry: upgrade-your-pre-commit-version + files: '' + minimum_pre_commit_version: 0.15.0 - id: check-added-large-files - name: Check for added large files - description: Prevent giant files from being committed - entry: check-added-large-files - language: python - # Match all files + language: system + name: upgrade-your-pre-commit-version + entry: upgrade-your-pre-commit-version files: '' + minimum_pre_commit_version: 0.15.0 - id: check-ast - name: Check python ast - description: Simply check whether the files parse as valid python. - entry: check-ast - language: python - files: '\.py$' + language: system + name: upgrade-your-pre-commit-version + entry: upgrade-your-pre-commit-version + files: '' + minimum_pre_commit_version: 0.15.0 +- id: check-builtin-literals + language: system + name: upgrade-your-pre-commit-version + entry: upgrade-your-pre-commit-version + files: '' + minimum_pre_commit_version: 0.15.0 - id: check-byte-order-marker - name: Check for byte-order marker - description: Forbid files which have a UTF-8 byte-order marker - entry: check-byte-order-marker - language: python - files: '\.py$' + language: system + name: upgrade-your-pre-commit-version + entry: upgrade-your-pre-commit-version + files: '' + minimum_pre_commit_version: 0.15.0 - id: check-case-conflict - name: Check for case conflicts - description: Check for files that would conflict in case-insensitive filesystems - entry: check-case-conflict - language: python - # Match all files + language: system + name: upgrade-your-pre-commit-version + entry: upgrade-your-pre-commit-version files: '' + minimum_pre_commit_version: 0.15.0 - id: check-docstring-first - name: Check docstring is first - description: Checks a common error of defining a docstring after code. - entry: check-docstring-first - language: python - files: \.py$ + language: system + name: upgrade-your-pre-commit-version + entry: upgrade-your-pre-commit-version + files: '' + minimum_pre_commit_version: 0.15.0 +- id: check-executables-have-shebangs + language: system + name: upgrade-your-pre-commit-version + entry: upgrade-your-pre-commit-version + files: '' + minimum_pre_commit_version: 0.15.0 - id: check-json - name: Check JSON - description: This hook checks json files for parseable syntax. - entry: check-json - language: python - files: \.json$ + language: system + name: upgrade-your-pre-commit-version + entry: upgrade-your-pre-commit-version + files: '' + minimum_pre_commit_version: 0.15.0 - id: pretty-format-json - name: Pretty format JSON - description: This hook sets a standard for formatting JSON files. - entry: pretty-format-json - language: python - files: \.json$ + language: system + name: upgrade-your-pre-commit-version + entry: upgrade-your-pre-commit-version + files: '' + minimum_pre_commit_version: 0.15.0 - id: check-merge-conflict - name: Check for merge conflicts - description: Check for files that contain merge conflict strings. - entry: check-merge-conflict - language: python - # Match all files + language: system + name: upgrade-your-pre-commit-version + entry: upgrade-your-pre-commit-version files: '' + minimum_pre_commit_version: 0.15.0 - id: check-symlinks - name: Check for broken symlinks - description: Checks for symlinks which do not point to anything. - entry: check-symlinks - language: python - # Match all files + language: system + name: upgrade-your-pre-commit-version + entry: upgrade-your-pre-commit-version files: '' + minimum_pre_commit_version: 0.15.0 +- id: check-vcs-permalinks + language: system + name: upgrade-your-pre-commit-version + entry: upgrade-your-pre-commit-version + files: '' + minimum_pre_commit_version: 0.15.0 - id: check-xml - name: Check Xml - description: This hook checks xml files for parseable syntax. - entry: check-xml - language: python - files: \.xml$ + language: system + name: upgrade-your-pre-commit-version + entry: upgrade-your-pre-commit-version + files: '' + minimum_pre_commit_version: 0.15.0 - id: check-yaml - name: Check Yaml - description: This hook checks yaml files for parseable syntax. - entry: check-yaml - language: python - files: \.(yaml|yml|eyaml)$ + language: system + name: upgrade-your-pre-commit-version + entry: upgrade-your-pre-commit-version + files: '' + minimum_pre_commit_version: 0.15.0 - id: debug-statements - name: Debug Statements (Python) - description: This hook checks that debug statements (pdb, ipdb, pudb) are not imported on commit. - entry: debug-statement-hook - language: python - files: \.py$ + language: system + name: upgrade-your-pre-commit-version + entry: upgrade-your-pre-commit-version + files: '' + minimum_pre_commit_version: 0.15.0 - id: detect-aws-credentials - name: Detect AWS Credentials - description: Detects *your* aws credentials from the aws cli credentials file - entry: detect-aws-credentials - language: python + language: system + name: upgrade-your-pre-commit-version + entry: upgrade-your-pre-commit-version files: '' + minimum_pre_commit_version: 0.15.0 - id: detect-private-key - name: Detect Private Key - description: Detects the presence of private keys - entry: detect-private-key - language: python + language: system + name: upgrade-your-pre-commit-version + entry: upgrade-your-pre-commit-version files: '' + minimum_pre_commit_version: 0.15.0 - id: double-quote-string-fixer - name: Fix double quoted strings - description: This hook replaces double quoted strings with single quoted strings - entry: double-quote-string-fixer - language: python - files: \.py$ -- id: end-of-file-fixer - name: Fix End of Files - description: Ensures that a file is either empty, or ends with one newline. - entry: end-of-file-fixer - language: python - files: \.(asciidoc|adoc|coffee|cpp|css|c|ejs|erb|groovy|h|haml|hh|hpp|hxx|html|in|j2|jade|json|js|less|markdown|md|ml|mli|pp|py|rb|rs|R|scala|scss|sh|slim|tex|tmpl|ts|txt|yaml|yml)$ -- id: fix-encoding-pragma - name: Fix python encoding pragma - language: python - entry: fix-encoding-pragma - description: 'Add # -*- coding: utf-8 -*- to the top of python files' - files: \.py$ -- id: flake8 - name: Flake8 - description: This hook runs flake8. - entry: flake8 - language: python - files: \.py$ -- id: forbid-new-submodules - name: Forbid new submodules - language: python - entry: forbid-new-submodules - description: Prevent addition of new git submodules + language: system + name: upgrade-your-pre-commit-version + entry: upgrade-your-pre-commit-version files: '' + minimum_pre_commit_version: 0.15.0 +- id: end-of-file-fixer + language: system + name: upgrade-your-pre-commit-version + entry: upgrade-your-pre-commit-version + files: '' + minimum_pre_commit_version: 0.15.0 +- id: file-contents-sorter + language: system + name: upgrade-your-pre-commit-version + entry: upgrade-your-pre-commit-version + files: '' + minimum_pre_commit_version: 0.15.0 +- id: fix-encoding-pragma + language: system + name: upgrade-your-pre-commit-version + entry: upgrade-your-pre-commit-version + files: '' + minimum_pre_commit_version: 0.15.0 +- id: flake8 + language: system + name: upgrade-your-pre-commit-version + entry: upgrade-your-pre-commit-version + files: '' + minimum_pre_commit_version: 0.15.0 +- id: forbid-new-submodules + language: system + name: upgrade-your-pre-commit-version + entry: upgrade-your-pre-commit-version + files: '' + minimum_pre_commit_version: 0.15.0 +- id: mixed-line-ending + language: system + name: upgrade-your-pre-commit-version + entry: upgrade-your-pre-commit-version + files: '' + minimum_pre_commit_version: 0.15.0 - id: name-tests-test - name: Tests should end in _test.py - description: This verifies that test files are named correctly - entry: name-tests-test - language: python - files: tests/.+\.py$ + language: system + name: upgrade-your-pre-commit-version + entry: upgrade-your-pre-commit-version + files: '' + minimum_pre_commit_version: 0.15.0 +- id: no-commit-to-branch + language: system + name: upgrade-your-pre-commit-version + entry: upgrade-your-pre-commit-version + files: '' + minimum_pre_commit_version: 0.15.0 - id: pyflakes - name: Pyflakes (DEPRECATED, use flake8) - description: This hook runs pyflakes. (This is deprecated, use flake8). - entry: pyflakes - language: python - files: \.py$ + language: system + name: upgrade-your-pre-commit-version + entry: upgrade-your-pre-commit-version + files: '' + minimum_pre_commit_version: 0.15.0 - id: requirements-txt-fixer - name: Fix requirements.txt - description: Sorts entries in requirements.txt - entry: requirements-txt-fixer - language: python - files: requirements.*\.txt$ + language: system + name: upgrade-your-pre-commit-version + entry: upgrade-your-pre-commit-version + files: '' + minimum_pre_commit_version: 0.15.0 +- id: sort-simple-yaml + language: system + name: upgrade-your-pre-commit-version + entry: upgrade-your-pre-commit-version + files: '' + minimum_pre_commit_version: 0.15.0 - id: trailing-whitespace - name: Trim Trailing Whitespace - description: This hook trims trailing whitespace. - entry: trailing-whitespace-fixer - language: python - files: \.(asciidoc|adoc|coffee|cpp|css|c|ejs|erb|groovy|h|haml|hh|hpp|hxx|html|in|j2|jade|json|js|less|markdown|md|ml|mli|pp|py|rb|rs|R|scala|scss|sh|slim|tex|tmpl|ts|txt|yaml|yml)$ + language: system + name: upgrade-your-pre-commit-version + entry: upgrade-your-pre-commit-version + files: '' + minimum_pre_commit_version: 0.15.0 diff --git a/pre_commit_hooks/autopep8_wrapper.py b/pre_commit_hooks/autopep8_wrapper.py index f6f55fb..b2d2d0c 100644 --- a/pre_commit_hooks/autopep8_wrapper.py +++ b/pre_commit_hooks/autopep8_wrapper.py @@ -14,12 +14,12 @@ def main(argv=None): retv = 0 for filename in args.files: - original_contents = io.open(filename).read() + original_contents = io.open(filename, encoding='UTF-8').read() new_contents = autopep8.fix_code(original_contents, args) if original_contents != new_contents: - print('Fixing {0}'.format(filename)) + print('Fixing {}'.format(filename)) retv = 1 - with io.open(filename, 'w') as output_file: + with io.open(filename, 'w', encoding='UTF-8') as output_file: output_file.write(new_contents) return retv diff --git a/pre_commit_hooks/check_added_large_files.py b/pre_commit_hooks/check_added_large_files.py index 5ef7f22..2d06706 100644 --- a/pre_commit_hooks/check_added_large_files.py +++ b/pre_commit_hooks/check_added_large_files.py @@ -4,6 +4,7 @@ from __future__ import print_function from __future__ import unicode_literals import argparse +import json import math import os @@ -13,23 +14,13 @@ from pre_commit_hooks.util import cmd_output def lfs_files(): - try: # pragma: no cover (no git-lfs) - lines = cmd_output('git', 'lfs', 'status', '--porcelain').splitlines() + try: + # Introduced in git-lfs 2.2.0, first working in 2.2.1 + lfs_ret = cmd_output('git', 'lfs', 'status', '--json') except CalledProcessError: # pragma: no cover (with git-lfs) - lines = [] + lfs_ret = '{"files":{}}' - modes_and_fileparts = [ - (line[:3].strip(), line[3:].rpartition(' ')[0]) for line in lines - ] - - def to_file_part(mode, filepart): # pragma: no cover (no git-lfs) - assert mode in ('A', 'R') - return filepart if mode == 'A' else filepart.split(' -> ')[1] - - return set( - to_file_part(mode, filepart) for mode, filepart in modes_and_fileparts - if mode in ('A', 'R') - ) + return set(json.loads(lfs_ret)['files']) def find_large_added_files(filenames, maxkb): @@ -41,7 +32,7 @@ def find_large_added_files(filenames, maxkb): for filename in filenames: kb = int(math.ceil(os.stat(filename).st_size / 1024)) if kb > maxkb: - print('{0} ({1} KB) exceeds {2} KB.'.format(filename, kb, maxkb)) + print('{} ({} KB) exceeds {} KB.'.format(filename, kb, maxkb)) retv = 1 return retv @@ -51,7 +42,7 @@ def main(argv=None): parser = argparse.ArgumentParser() parser.add_argument( 'filenames', nargs='*', - help='Filenames pre-commit believes are changed.' + help='Filenames pre-commit believes are changed.', ) parser.add_argument( '--maxkb', type=int, default=500, diff --git a/pre_commit_hooks/check_ast.py b/pre_commit_hooks/check_ast.py index c993e6a..1090300 100644 --- a/pre_commit_hooks/check_ast.py +++ b/pre_commit_hooks/check_ast.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals import argparse import ast -import os.path +import platform import sys import traceback @@ -14,19 +14,19 @@ def check_ast(argv=None): parser.add_argument('filenames', nargs='*') args = parser.parse_args(argv) - _, interpreter = os.path.split(sys.executable) - retval = 0 for filename in args.filenames: try: ast.parse(open(filename, 'rb').read(), filename=filename) except SyntaxError: - print('{0}: failed parsing with {1}:'.format( - filename, interpreter, + print('{}: failed parsing with {} {}:'.format( + filename, + platform.python_implementation(), + sys.version.partition(' ')[0], )) - print('\n{0}'.format( - ' ' + traceback.format_exc().replace('\n', '\n ') + print('\n{}'.format( + ' ' + traceback.format_exc().replace('\n', '\n '), )) retval = 1 return retval diff --git a/pre_commit_hooks/check_builtin_literals.py b/pre_commit_hooks/check_builtin_literals.py new file mode 100644 index 0000000..b7f0c00 --- /dev/null +++ b/pre_commit_hooks/check_builtin_literals.py @@ -0,0 +1,95 @@ +from __future__ import unicode_literals + +import argparse +import ast +import collections +import sys + + +BUILTIN_TYPES = { + 'complex': '0j', + 'dict': '{}', + 'float': '0.0', + 'int': '0', + 'list': '[]', + 'str': "''", + 'tuple': '()', +} + + +BuiltinTypeCall = collections.namedtuple('BuiltinTypeCall', ['name', 'line', 'column']) + + +class BuiltinTypeVisitor(ast.NodeVisitor): + def __init__(self, ignore=None, allow_dict_kwargs=True): + self.builtin_type_calls = [] + self.ignore = set(ignore) if ignore else set() + self.allow_dict_kwargs = allow_dict_kwargs + + def _check_dict_call(self, node): + return self.allow_dict_kwargs and (getattr(node, 'kwargs', None) or getattr(node, 'keywords', None)) + + def visit_Call(self, node): + if not isinstance(node.func, ast.Name): + # Ignore functions that are object attributes (`foo.bar()`). + # Assume that if the user calls `builtins.list()`, they know what + # they're doing. + return + if node.func.id not in set(BUILTIN_TYPES).difference(self.ignore): + return + if node.func.id == 'dict' and self._check_dict_call(node): + return + elif node.args: + return + self.builtin_type_calls.append( + BuiltinTypeCall(node.func.id, node.lineno, node.col_offset), + ) + + +def check_file_for_builtin_type_constructors(filename, ignore=None, allow_dict_kwargs=True): + tree = ast.parse(open(filename, 'rb').read(), filename=filename) + visitor = BuiltinTypeVisitor(ignore=ignore, allow_dict_kwargs=allow_dict_kwargs) + visitor.visit(tree) + return visitor.builtin_type_calls + + +def parse_args(argv): + def parse_ignore(value): + return set(value.split(',')) + + parser = argparse.ArgumentParser() + parser.add_argument('filenames', nargs='*') + parser.add_argument('--ignore', type=parse_ignore, default=set()) + + allow_dict_kwargs = parser.add_mutually_exclusive_group(required=False) + allow_dict_kwargs.add_argument('--allow-dict-kwargs', action='store_true') + allow_dict_kwargs.add_argument('--no-allow-dict-kwargs', dest='allow_dict_kwargs', action='store_false') + allow_dict_kwargs.set_defaults(allow_dict_kwargs=True) + + return parser.parse_args(argv) + + +def main(argv=None): + args = parse_args(argv) + rc = 0 + for filename in args.filenames: + calls = check_file_for_builtin_type_constructors( + filename, + ignore=args.ignore, + allow_dict_kwargs=args.allow_dict_kwargs, + ) + if calls: + rc = rc or 1 + for call in calls: + print( + '{filename}:{call.line}:{call.column} - Replace {call.name}() with {replacement}'.format( + filename=filename, + call=call, + replacement=BUILTIN_TYPES[call.name], + ), + ) + return rc + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/pre_commit_hooks/check_byte_order_marker.py b/pre_commit_hooks/check_byte_order_marker.py index 274f949..1541b30 100644 --- a/pre_commit_hooks/check_byte_order_marker.py +++ b/pre_commit_hooks/check_byte_order_marker.py @@ -16,7 +16,7 @@ def main(argv=None): with open(filename, 'rb') as f: if f.read(3) == b'\xef\xbb\xbf': retv = 1 - print('{0}: Has a byte-order marker'.format(filename)) + print('{}: Has a byte-order marker'.format(filename)) return retv diff --git a/pre_commit_hooks/check_case_conflict.py b/pre_commit_hooks/check_case_conflict.py index dd4ad86..0f78296 100644 --- a/pre_commit_hooks/check_case_conflict.py +++ b/pre_commit_hooks/check_case_conflict.py @@ -9,7 +9,7 @@ from pre_commit_hooks.util import cmd_output def lower_set(iterable): - return set(x.lower() for x in iterable) + return {x.lower() for x in iterable} def find_conflicting_filenames(filenames): @@ -35,7 +35,7 @@ def find_conflicting_filenames(filenames): if x.lower() in conflicts ] for filename in sorted(conflicting_files): - print('Case-insensitivity conflict found: {0}'.format(filename)) + print('Case-insensitivity conflict found: {}'.format(filename)) retv = 1 return retv @@ -45,7 +45,7 @@ def main(argv=None): parser = argparse.ArgumentParser() parser.add_argument( 'filenames', nargs='*', - help='Filenames pre-commit believes are changed.' + help='Filenames pre-commit believes are changed.', ) args = parser.parse_args(argv) diff --git a/pre_commit_hooks/check_docstring_first.py b/pre_commit_hooks/check_docstring_first.py index da5425d..0896812 100644 --- a/pre_commit_hooks/check_docstring_first.py +++ b/pre_commit_hooks/check_docstring_first.py @@ -1,4 +1,5 @@ from __future__ import absolute_import +from __future__ import print_function from __future__ import unicode_literals import argparse @@ -27,18 +28,18 @@ def check_docstring_first(src, filename=''): if tok_type == tokenize.STRING and scol == 0: if found_docstring_line is not None: print( - '{0}:{1} Multiple module docstrings ' - '(first docstring on line {2}).'.format( + '{}:{} Multiple module docstrings ' + '(first docstring on line {}).'.format( filename, sline, found_docstring_line, - ) + ), ) return 1 elif found_code_line is not None: print( - '{0}:{1} Module docstring appears after code ' - '(code seen on line {2}).'.format( + '{}:{} Module docstring appears after code ' + '(code seen on line {}).'.format( filename, sline, found_code_line, - ) + ), ) return 1 else: @@ -57,7 +58,7 @@ def main(argv=None): retv = 0 for filename in args.filenames: - contents = io.open(filename).read() + contents = io.open(filename, encoding='UTF-8').read() retv |= check_docstring_first(contents, filename=filename) return retv diff --git a/pre_commit_hooks/check_executables_have_shebangs.py b/pre_commit_hooks/check_executables_have_shebangs.py new file mode 100644 index 0000000..89ac6e5 --- /dev/null +++ b/pre_commit_hooks/check_executables_have_shebangs.py @@ -0,0 +1,40 @@ +"""Check that executable text files have a shebang.""" +from __future__ import absolute_import +from __future__ import print_function +from __future__ import unicode_literals + +import argparse +import pipes +import sys + + +def check_has_shebang(path): + with open(path, 'rb') as f: + first_bytes = f.read(2) + + if first_bytes != b'#!': + print( + '{path}: marked executable but has no (or invalid) shebang!\n' + " If it isn't supposed to be executable, try: chmod -x {quoted}\n" + ' If it is supposed to be executable, double-check its shebang.'.format( + path=path, + quoted=pipes.quote(path), + ), + file=sys.stderr, + ) + return 1 + else: + return 0 + + +def main(argv=None): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('filenames', nargs='*') + args = parser.parse_args(argv) + + retv = 0 + + for filename in args.filenames: + retv |= check_has_shebang(filename) + + return retv diff --git a/pre_commit_hooks/check_json.py b/pre_commit_hooks/check_json.py index 688f719..b403f4b 100644 --- a/pre_commit_hooks/check_json.py +++ b/pre_commit_hooks/check_json.py @@ -1,10 +1,10 @@ from __future__ import print_function import argparse +import io +import json import sys -import simplejson - def check_json(argv=None): parser = argparse.ArgumentParser() @@ -14,9 +14,9 @@ def check_json(argv=None): retval = 0 for filename in args.filenames: try: - simplejson.load(open(filename)) - except (simplejson.JSONDecodeError, UnicodeDecodeError) as exc: - print('{0}: Failed to json decode ({1})'.format(filename, exc)) + json.load(io.open(filename, encoding='UTF-8')) + except (ValueError, UnicodeDecodeError) as exc: + print('{}: Failed to json decode ({})'.format(filename, exc)) retval = 1 return retval diff --git a/pre_commit_hooks/check_merge_conflict.py b/pre_commit_hooks/check_merge_conflict.py index d986998..5035b6d 100644 --- a/pre_commit_hooks/check_merge_conflict.py +++ b/pre_commit_hooks/check_merge_conflict.py @@ -7,7 +7,7 @@ CONFLICT_PATTERNS = [ b'<<<<<<< ', b'======= ', b'=======\n', - b'>>>>>>> ' + b'>>>>>>> ', ] WARNING_MSG = 'Merge conflict string "{0}" found in {1}:{2}' @@ -15,7 +15,11 @@ WARNING_MSG = 'Merge conflict string "{0}" found in {1}:{2}' def is_in_merge(): return ( os.path.exists(os.path.join('.git', 'MERGE_MSG')) and - os.path.exists(os.path.join('.git', 'MERGE_HEAD')) + ( + os.path.exists(os.path.join('.git', 'MERGE_HEAD')) or + os.path.exists(os.path.join('.git', 'rebase-apply')) or + os.path.exists(os.path.join('.git', 'rebase-merge')) + ) ) diff --git a/pre_commit_hooks/check_symlinks.py b/pre_commit_hooks/check_symlinks.py index 713dd83..fd80089 100644 --- a/pre_commit_hooks/check_symlinks.py +++ b/pre_commit_hooks/check_symlinks.py @@ -19,7 +19,7 @@ def check_symlinks(argv=None): os.path.islink(filename) and not os.path.exists(filename) ): # pragma: no cover (symlink support required) - print('{0}: Broken symlink'.format(filename)) + print('{}: Broken symlink'.format(filename)) retv = 1 return retv diff --git a/pre_commit_hooks/check_vcs_permalinks.py b/pre_commit_hooks/check_vcs_permalinks.py new file mode 100644 index 0000000..139b41a --- /dev/null +++ b/pre_commit_hooks/check_vcs_permalinks.py @@ -0,0 +1,44 @@ +from __future__ import absolute_import +from __future__ import print_function +from __future__ import unicode_literals + +import argparse +import re +import sys + + +GITHUB_NON_PERMALINK = re.compile( + b'https://github.com/[^/ ]+/[^/ ]+/blob/master/[^# ]+#L\d+', +) + + +def _check_filename(filename): + retv = 0 + with open(filename, 'rb') as f: + for i, line in enumerate(f, 1): + if GITHUB_NON_PERMALINK.search(line): + sys.stdout.write('{}:{}:'.format(filename, i)) + sys.stdout.flush() + getattr(sys.stdout, 'buffer', sys.stdout).write(line) + retv = 1 + return retv + + +def main(argv=None): + parser = argparse.ArgumentParser() + parser.add_argument('filenames', nargs='*') + args = parser.parse_args(argv) + + retv = 0 + for filename in args.filenames: + retv |= _check_filename(filename) + + if retv: + print() + print('Non-permanent github link detected.') + print('On any page on github press [y] to load a permalink.') + return retv + + +if __name__ == '__main__': + exit(main()) diff --git a/pre_commit_hooks/check_xml.py b/pre_commit_hooks/check_xml.py index 926f6e9..a4c11a5 100644 --- a/pre_commit_hooks/check_xml.py +++ b/pre_commit_hooks/check_xml.py @@ -19,7 +19,7 @@ def check_xml(argv=None): with io.open(filename, 'rb') as xml_file: xml.sax.parse(xml_file, xml.sax.ContentHandler()) except xml.sax.SAXException as exc: - print('{0}: Failed to xml parse ({1})'.format(filename, exc)) + print('{}: Failed to xml parse ({})'.format(filename, exc)) retval = 1 return retval diff --git a/pre_commit_hooks/check_yaml.py b/pre_commit_hooks/check_yaml.py index 36fd128..153adda 100644 --- a/pre_commit_hooks/check_yaml.py +++ b/pre_commit_hooks/check_yaml.py @@ -1,6 +1,7 @@ from __future__ import print_function import argparse +import collections import sys import yaml @@ -11,21 +12,57 @@ except ImportError: # pragma: no cover (no libyaml-dev / pypy) Loader = yaml.SafeLoader +def _exhaust(gen): + for _ in gen: + pass + + +def _parse_unsafe(*args, **kwargs): + _exhaust(yaml.parse(*args, **kwargs)) + + +def _load_all(*args, **kwargs): + _exhaust(yaml.load_all(*args, **kwargs)) + + +Key = collections.namedtuple('Key', ('multi', 'unsafe')) +LOAD_FNS = { + Key(multi=False, unsafe=False): yaml.load, + Key(multi=False, unsafe=True): _parse_unsafe, + Key(multi=True, unsafe=False): _load_all, + Key(multi=True, unsafe=True): _parse_unsafe, +} + + def check_yaml(argv=None): parser = argparse.ArgumentParser() + parser.add_argument( + '-m', '--multi', '--allow-multiple-documents', action='store_true', + ) + parser.add_argument( + '--unsafe', action='store_true', + help=( + 'Instead of loading the files, simply parse them for syntax. ' + 'A syntax-only check enables extensions and unsafe contstructs ' + 'which would otherwise be forbidden. Using this option removes ' + 'all guarantees of portability to other yaml implementations. ' + 'Implies --allow-multiple-documents' + ), + ) parser.add_argument('--ignore-tags', type=lambda s: s.split(','), default=[], help='Custom tags to ignore.') parser.add_argument('filenames', nargs='*', help='Yaml filenames to check.') args = parser.parse_args(argv) - # Ignore custom tags by returning None for tag in args.ignore_tags: Loader.add_constructor(tag, lambda *a, **k: None) + load_fn = LOAD_FNS[Key(multi=args.multi, unsafe=args.unsafe)] + retval = 0 for filename in args.filenames: try: - yaml.load(open(filename), Loader=Loader) + load_fn(open(filename), Loader=Loader) except yaml.YAMLError as exc: print(exc) retval = 1 diff --git a/pre_commit_hooks/debug_statement_hook.py b/pre_commit_hooks/debug_statement_hook.py index 52fe72e..81591dd 100644 --- a/pre_commit_hooks/debug_statement_hook.py +++ b/pre_commit_hooks/debug_statement_hook.py @@ -7,69 +7,66 @@ import collections import traceback -DEBUG_STATEMENTS = set(['pdb', 'ipdb', 'pudb', 'q', 'rdb']) +DEBUG_STATEMENTS = {'pdb', 'ipdb', 'pudb', 'q', 'rdb'} +Debug = collections.namedtuple('Debug', ('line', 'col', 'name', 'reason')) -DebugStatement = collections.namedtuple( - 'DebugStatement', ['name', 'line', 'col'], -) - - -class ImportStatementParser(ast.NodeVisitor): +class DebugStatementParser(ast.NodeVisitor): def __init__(self): - self.debug_import_statements = [] + self.breakpoints = [] def visit_Import(self, node): - for node_name in node.names: - if node_name.name in DEBUG_STATEMENTS: - self.debug_import_statements.append( - DebugStatement(node_name.name, node.lineno, node.col_offset), - ) + for name in node.names: + if name.name in DEBUG_STATEMENTS: + st = Debug(node.lineno, node.col_offset, name.name, 'imported') + self.breakpoints.append(st) def visit_ImportFrom(self, node): if node.module in DEBUG_STATEMENTS: - self.debug_import_statements.append( - DebugStatement(node.module, node.lineno, node.col_offset) - ) + st = Debug(node.lineno, node.col_offset, node.module, 'imported') + self.breakpoints.append(st) + + def visit_Call(self, node): + """python3.7+ breakpoint()""" + if isinstance(node.func, ast.Name) and node.func.id == 'breakpoint': + st = Debug(node.lineno, node.col_offset, node.func.id, 'called') + self.breakpoints.append(st) + self.generic_visit(node) -def check_file_for_debug_statements(filename): +def check_file(filename): try: - ast_obj = ast.parse(open(filename).read(), filename=filename) + ast_obj = ast.parse(open(filename, 'rb').read(), filename=filename) except SyntaxError: - print('{0} - Could not parse ast'.format(filename)) + print('{} - Could not parse ast'.format(filename)) print() print('\t' + traceback.format_exc().replace('\n', '\n\t')) print() return 1 - visitor = ImportStatementParser() + + visitor = DebugStatementParser() visitor.visit(ast_obj) - if visitor.debug_import_statements: - for debug_statement in visitor.debug_import_statements: - print( - '{0}:{1}:{2} - {3} imported'.format( - filename, - debug_statement.line, - debug_statement.col, - debug_statement.name, - ) - ) - return 1 - else: - return 0 + + for bp in visitor.breakpoints: + print( + '{}:{}:{} - {} {}'.format( + filename, bp.line, bp.col, bp.name, bp.reason, + ), + ) + + return int(bool(visitor.breakpoints)) -def debug_statement_hook(argv=None): +def main(argv=None): parser = argparse.ArgumentParser() parser.add_argument('filenames', nargs='*', help='Filenames to run') args = parser.parse_args(argv) retv = 0 for filename in args.filenames: - retv |= check_file_for_debug_statements(filename) - + retv |= check_file(filename) return retv if __name__ == '__main__': - exit(debug_statement_hook()) + exit(main()) diff --git a/pre_commit_hooks/detect_aws_credentials.py b/pre_commit_hooks/detect_aws_credentials.py index 42758f0..47a69bf 100644 --- a/pre_commit_hooks/detect_aws_credentials.py +++ b/pre_commit_hooks/detect_aws_credentials.py @@ -12,7 +12,7 @@ def get_aws_credential_files_from_env(): files = set() for env_var in ( 'AWS_CONFIG_FILE', 'AWS_CREDENTIAL_FILE', 'AWS_SHARED_CREDENTIALS_FILE', - 'BOTO_CONFIG' + 'BOTO_CONFIG', ): if env_var in os.environ: files.add(os.environ[env_var]) @@ -23,7 +23,7 @@ def get_aws_secrets_from_env(): """Extract AWS secrets from environment variables.""" keys = set() 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 env_var in os.environ: keys.add(os.environ[env_var]) @@ -50,10 +50,12 @@ def get_aws_secrets_from_file(credentials_file): for section in parser.sections(): for var in ( 'aws_secret_access_key', 'aws_security_token', - 'aws_session_token' + 'aws_session_token', ): try: - keys.add(parser.get(section, var)) + key = parser.get(section, var).strip() + if key: + keys.add(key) except configparser.NoOptionError: pass return keys @@ -93,13 +95,13 @@ def main(argv=None): help=( 'Location of additional AWS credential files from which to get ' 'secret keys from' - ) + ), ) parser.add_argument( '--allow-missing-credentials', dest='allow_missing_credentials', action='store_true', - help='Allow hook to pass when no credentials are detected.' + help='Allow hook to pass when no credentials are detected.', ) args = parser.parse_args(argv) @@ -124,7 +126,7 @@ def main(argv=None): print( 'No AWS keys were found in the configured credential files and ' 'environment variables.\nPlease ensure you have the correct ' - 'setting for --credentials-file' + 'setting for --credentials-file', ) return 2 diff --git a/pre_commit_hooks/detect_private_key.py b/pre_commit_hooks/detect_private_key.py index d187364..693e59e 100644 --- a/pre_commit_hooks/detect_private_key.py +++ b/pre_commit_hooks/detect_private_key.py @@ -8,6 +8,9 @@ BLACKLIST = [ b'BEGIN DSA PRIVATE KEY', b'BEGIN EC PRIVATE KEY', b'BEGIN OPENSSH PRIVATE KEY', + b'BEGIN PRIVATE KEY', + b'PuTTY-User-Key-File-2', + b'BEGIN SSH2 ENCRYPTED PRIVATE KEY', ] @@ -26,7 +29,7 @@ def detect_private_key(argv=None): if private_key_files: for private_key_file in private_key_files: - print('Private key found: {0}'.format(private_key_file)) + print('Private key found: {}'.format(private_key_file)) return 1 else: return 0 diff --git a/pre_commit_hooks/end_of_file_fixer.py b/pre_commit_hooks/end_of_file_fixer.py index 3349d83..4fe82b7 100644 --- a/pre_commit_hooks/end_of_file_fixer.py +++ b/pre_commit_hooks/end_of_file_fixer.py @@ -58,7 +58,7 @@ def end_of_file_fixer(argv=None): with open(filename, 'rb+') as file_obj: ret_for_file = fix_file(file_obj) if ret_for_file: - print('Fixing {0}'.format(filename)) + print('Fixing {}'.format(filename)) retv |= ret_for_file return retv diff --git a/pre_commit_hooks/file_contents_sorter.py b/pre_commit_hooks/file_contents_sorter.py new file mode 100644 index 0000000..fe7f7ee --- /dev/null +++ b/pre_commit_hooks/file_contents_sorter.py @@ -0,0 +1,52 @@ +""" +A very simple pre-commit hook that, when passed one or more filenames +as arguments, will sort the lines in those files. + +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 +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 +this hook on that file should reduce the instances of git merge +conflicts and keep the file nicely ordered. +""" +from __future__ import print_function + +import argparse + +PASS = 0 +FAIL = 1 + + +def sort_file_contents(f): + before = list(f) + after = sorted([line.strip(b'\n\r') for line in before if line.strip()]) + + before_string = b''.join(before) + after_string = b'\n'.join(after) + b'\n' + + if before_string == after_string: + return PASS + else: + f.seek(0) + f.write(after_string) + f.truncate() + return FAIL + + +def main(argv=None): + parser = argparse.ArgumentParser() + parser.add_argument('filenames', nargs='+', help='Files to sort') + args = parser.parse_args(argv) + + retv = PASS + + for arg in args.filenames: + with open(arg, 'rb+') as file_obj: + ret_for_file = sort_file_contents(file_obj) + + if ret_for_file: + print('Sorting {}'.format(arg)) + + retv |= ret_for_file + + return retv diff --git a/pre_commit_hooks/mixed_line_ending.py b/pre_commit_hooks/mixed_line_ending.py new file mode 100644 index 0000000..a163726 --- /dev/null +++ b/pre_commit_hooks/mixed_line_ending.py @@ -0,0 +1,84 @@ +from __future__ import absolute_import +from __future__ import print_function +from __future__ import unicode_literals + +import argparse +import collections + + +CRLF = b'\r\n' +LF = b'\n' +CR = b'\r' +# Prefer LF to CRLF to CR, but detect CRLF before LF +ALL_ENDINGS = (CR, CRLF, LF) +FIX_TO_LINE_ENDING = {'cr': CR, 'crlf': CRLF, 'lf': LF} + + +def _fix(filename, contents, ending): + new_contents = b''.join( + line.rstrip(b'\r\n') + ending for line in contents.splitlines(True) + ) + with open(filename, 'wb') as f: + f.write(new_contents) + + +def fix_filename(filename, fix): + with open(filename, 'rb') as f: + contents = f.read() + + counts = collections.defaultdict(int) + + for line in contents.splitlines(True): + for ending in ALL_ENDINGS: + if line.endswith(ending): + counts[ending] += 1 + break + + # Some amount of mixed line endings + mixed = sum(bool(x) for x in counts.values()) > 1 + + if fix == 'no' or (fix == 'auto' and not mixed): + return mixed + + if fix == 'auto': + max_ending = LF + max_lines = 0 + # ordering is important here such that lf > crlf > cr + for ending_type in ALL_ENDINGS: + # also important, using >= to find a max that prefers the last + if counts[ending_type] >= max_lines: + max_ending = ending_type + max_lines = counts[ending_type] + + _fix(filename, contents, max_ending) + return 1 + else: + target_ending = FIX_TO_LINE_ENDING[fix] + # find if there are lines with *other* endings + # It's possible there's no line endings of the target type + counts.pop(target_ending, None) + other_endings = bool(sum(counts.values())) + if other_endings: + _fix(filename, contents, target_ending) + return other_endings + + +def main(argv=None): + parser = argparse.ArgumentParser() + parser.add_argument( + '-f', '--fix', + choices=('auto', 'no') + tuple(FIX_TO_LINE_ENDING), + default='auto', + help='Replace line ending with the specified. Default is "auto"', + ) + parser.add_argument('filenames', nargs='*', help='Filenames to fix') + args = parser.parse_args(argv) + + retv = 0 + for filename in args.filenames: + retv |= fix_filename(filename, args.fix) + return retv + + +if __name__ == '__main__': + exit(main()) diff --git a/pre_commit_hooks/no_commit_to_branch.py b/pre_commit_hooks/no_commit_to_branch.py new file mode 100644 index 0000000..fdd146b --- /dev/null +++ b/pre_commit_hooks/no_commit_to_branch.py @@ -0,0 +1,31 @@ +from __future__ import print_function + +import argparse + +from pre_commit_hooks.util import CalledProcessError +from pre_commit_hooks.util import cmd_output + + +def is_on_branch(protected): + try: + branch = cmd_output('git', 'symbolic-ref', 'HEAD') + except CalledProcessError: + return False + chunks = branch.strip().split('/') + return '/'.join(chunks[2:]) in protected + + +def main(argv=None): + parser = argparse.ArgumentParser() + parser.add_argument( + '-b', '--branch', action='append', + help='branch to disallow commits to, may be specified multiple times', + ) + args = parser.parse_args(argv) + + protected = set(args.branch or ('master',)) + return int(is_on_branch(protected)) + + +if __name__ == '__main__': + exit(main()) diff --git a/pre_commit_hooks/pretty_format_json.py b/pre_commit_hooks/pretty_format_json.py index 91dae8d..fb1c487 100644 --- a/pre_commit_hooks/pretty_format_json.py +++ b/pre_commit_hooks/pretty_format_json.py @@ -1,13 +1,15 @@ from __future__ import print_function import argparse +import io +import json import sys from collections import OrderedDict -import simplejson +from six import text_type -def _get_pretty_format(contents, indent, sort_keys=True, top_keys=[]): +def _get_pretty_format(contents, indent, ensure_ascii=True, sort_keys=True, top_keys=[]): def pairs_first(pairs): before = [pair for pair in pairs if pair[0] in top_keys] before = sorted(before, key=lambda x: top_keys.index(x[0])) @@ -15,39 +17,28 @@ def _get_pretty_format(contents, indent, sort_keys=True, top_keys=[]): if sort_keys: after = sorted(after, key=lambda x: x[0]) return OrderedDict(before + after) - return simplejson.dumps( - simplejson.loads( - contents, - object_pairs_hook=pairs_first, - ), - indent=indent - ) + "\n" # dumps don't end with a newline + json_pretty = json.dumps( + json.loads(contents, object_pairs_hook=pairs_first), + indent=indent, + ensure_ascii=ensure_ascii, + separators=(',', ': '), # Workaround for https://bugs.python.org/issue16333 + ) + # Ensure unicode (Py2) and add the newline that dumps does not end with. + return text_type(json_pretty) + '\n' def _autofix(filename, new_contents): - print("Fixing file {0}".format(filename)) - with open(filename, 'w') as f: + print('Fixing file {}'.format(filename)) + with io.open(filename, 'w', encoding='UTF-8') as f: f.write(new_contents) -def parse_indent(s): - # type: (str) -> str +def parse_num_to_int(s): + """Convert string numbers to int, leaving strings as is.""" try: - int_indentation_spec = int(s) + return int(s) except ValueError: - if not s.strip(): - return s - else: - raise ValueError( - 'Non-whitespace JSON indentation delimiter supplied. ', - ) - else: - if int_indentation_spec >= 0: - return int_indentation_spec * ' ' - else: - raise ValueError( - 'Negative integer supplied to construct JSON indentation delimiter. ', - ) + return s def parse_topkeys(s): @@ -65,9 +56,19 @@ def pretty_format_json(argv=None): ) parser.add_argument( '--indent', - type=parse_indent, - default=' ', - help='String used as delimiter for one indentation level', + type=parse_num_to_int, + default='2', + help=( + 'The number of indent spaces or a string to be used as delimiter' + ' for indentation level e.g. 4 or "\t" (Default: 2)' + ), + ) + parser.add_argument( + '--no-ensure-ascii', + action='store_true', + dest='no_ensure_ascii', + default=False, + help='Do NOT convert non-ASCII characters to Unicode escape sequences (\\uXXXX)', ) parser.add_argument( '--no-sort-keys', @@ -90,27 +91,26 @@ def pretty_format_json(argv=None): status = 0 for json_file in args.filenames: - with open(json_file) as f: + with io.open(json_file, encoding='UTF-8') as f: contents = f.read() try: pretty_contents = _get_pretty_format( - contents, args.indent, sort_keys=not args.no_sort_keys, - top_keys=args.top_keys + contents, args.indent, ensure_ascii=not args.no_ensure_ascii, + sort_keys=not args.no_sort_keys, top_keys=args.top_keys, ) if contents != pretty_contents: - print("File {0} is not pretty-formatted".format(json_file)) + print('File {} is not pretty-formatted'.format(json_file)) if args.autofix: _autofix(json_file, pretty_contents) status = 1 - - except simplejson.JSONDecodeError: + except ValueError: print( - "Input File {0} is not a valid JSON, consider using check-json" - .format(json_file) + 'Input File {} is not a valid JSON, consider using check-json' + .format(json_file), ) return 1 diff --git a/pre_commit_hooks/requirements_txt_fixer.py b/pre_commit_hooks/requirements_txt_fixer.py index 64b5e47..6dcf8d0 100644 --- a/pre_commit_hooks/requirements_txt_fixer.py +++ b/pre_commit_hooks/requirements_txt_fixer.py @@ -3,6 +3,10 @@ from __future__ import print_function import argparse +PASS = 0 +FAIL = 1 + + class Requirement(object): def __init__(self): @@ -30,21 +34,25 @@ class Requirement(object): def fix_requirements(f): requirements = [] - before = [] + before = tuple(f) after = [] - for line in f: - before.append(line) + before_string = b''.join(before) - # If the most recent requirement object has a value, then it's time to - # start building the next requirement object. + # If the file is empty (i.e. only whitespace/newlines) exit early + if before_string.strip() == b'': + return PASS + + for line in before: + # If the most recent requirement object has a value, then it's + # time to start building the next requirement object. if not len(requirements) or requirements[-1].value is not None: requirements.append(Requirement()) requirement = requirements[-1] - # If we see a newline before any requirements, then this is a top of - # file comment. + # If we see a newline before any requirements, then this is a + # top of file comment. if len(requirements) == 1 and line.strip() == b'': if len(requirement.comments) and requirement.comments[0].startswith(b'#'): requirement.value = b'\n' @@ -55,21 +63,33 @@ def fix_requirements(f): else: requirement.value = line - for requirement in sorted(requirements): - for comment in requirement.comments: - after.append(comment) - after.append(requirement.value) + # if a file ends in a comment, preserve it at the end + if requirements[-1].value is None: + rest = requirements.pop().comments + else: + rest = [] + + # find and remove pkg-resources==0.0.0 + # which is automatically added by broken pip package under Debian + requirements = [ + req for req in requirements + if req.value != b'pkg-resources==0.0.0\n' + ] + + for requirement in sorted(requirements): + after.extend(requirement.comments) + after.append(requirement.value) + after.extend(rest) - before_string = b''.join(before) after_string = b''.join(after) if before_string == after_string: - return 0 + return PASS else: f.seek(0) f.write(after_string) f.truncate() - return 1 + return FAIL def fix_requirements_txt(argv=None): @@ -77,14 +97,14 @@ def fix_requirements_txt(argv=None): parser.add_argument('filenames', nargs='*', help='Filenames to fix') args = parser.parse_args(argv) - retv = 0 + retv = PASS for arg in args.filenames: with open(arg, 'rb+') as file_obj: ret_for_file = fix_requirements(file_obj) if ret_for_file: - print('Sorting {0}'.format(arg)) + print('Sorting {}'.format(arg)) retv |= ret_for_file diff --git a/pre_commit_hooks/sort_simple_yaml.py b/pre_commit_hooks/sort_simple_yaml.py new file mode 100755 index 0000000..7afae91 --- /dev/null +++ b/pre_commit_hooks/sort_simple_yaml.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python +"""Sort a simple YAML file, keeping blocks of comments and definitions +together. + +We assume a strict subset of YAML that looks like: + + # block of header comments + # here that should always + # be at the top of the file + + # optional comments + # can go here + key: value + key: value + + key: value + +In other words, we don't sort deeper than the top layer, and might corrupt +complicated YAML files. +""" +from __future__ import print_function + +import argparse + + +QUOTES = ["'", '"'] + + +def sort(lines): + """Sort a YAML file in alphabetical order, keeping blocks together. + + :param lines: array of strings (without newlines) + :return: sorted array of strings + """ + # make a copy of lines since we will clobber it + lines = list(lines) + new_lines = parse_block(lines, header=True) + + for block in sorted(parse_blocks(lines), key=first_key): + if new_lines: + new_lines.append('') + new_lines.extend(block) + + return new_lines + + +def parse_block(lines, header=False): + """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 + comment. Otherwise, we stop after reaching an empty line. + + :param lines: list of lines + :param header: whether we are parsing a header block + :return: list of lines that form the single block + """ + block_lines = [] + while lines and lines[0] and (not header or lines[0].startswith('#')): + block_lines.append(lines.pop(0)) + return block_lines + + +def parse_blocks(lines): + """Parse and return all possible blocks, popping off the start of `lines`. + + :param lines: list of lines + :return: list of blocks, where each block is a list of lines + """ + blocks = [] + + while lines: + if lines[0] == '': + lines.pop(0) + else: + blocks.append(parse_block(lines)) + + return blocks + + +def first_key(lines): + """Returns a string representing the sort key of a block. + + The sort key is the first YAML key we encounter, ignoring comments, and + stripping leading quotes. + + >>> print(test) + # some comment + 'foo': true + >>> first_key(test) + 'foo' + """ + for line in lines: + if line.startswith('#'): + continue + if any(line.startswith(quote) for quote in QUOTES): + return line[1:] + return line + + +def main(argv=None): + parser = argparse.ArgumentParser() + parser.add_argument('filenames', nargs='*', help='Filenames to fix') + args = parser.parse_args(argv) + + retval = 0 + + for filename in args.filenames: + with open(filename, 'r+') as f: + lines = [line.rstrip() for line in f.readlines()] + new_lines = sort(lines) + + if lines != new_lines: + print("Fixing file `{filename}`".format(filename=filename)) + f.seek(0) + f.write("\n".join(new_lines) + "\n") + f.truncate() + retval = 1 + + return retval + + +if __name__ == '__main__': + exit(main()) diff --git a/pre_commit_hooks/string_fixer.py b/pre_commit_hooks/string_fixer.py index 9ef7a37..f73f09d 100644 --- a/pre_commit_hooks/string_fixer.py +++ b/pre_commit_hooks/string_fixer.py @@ -32,7 +32,7 @@ def get_line_offsets_by_line_no(src): def fix_strings(filename): - contents = io.open(filename).read() + contents = io.open(filename, encoding='UTF-8').read() line_offsets = get_line_offsets_by_line_no(contents) # Basically a mutable string @@ -52,7 +52,7 @@ def fix_strings(filename): new_contents = ''.join(splitcontents) if contents != new_contents: - with io.open(filename, 'w') as write_handle: + with io.open(filename, 'w', encoding='UTF-8') as write_handle: write_handle.write(new_contents) return 1 else: @@ -69,7 +69,7 @@ def main(argv=None): for filename in args.filenames: return_value = fix_strings(filename) if return_value != 0: - print('Fixing strings in {0}'.format(filename)) + print('Fixing strings in {}'.format(filename)) retv |= return_value return retv diff --git a/pre_commit_hooks/tests_should_end_in_test.py b/pre_commit_hooks/tests_should_end_in_test.py index 4bfc767..9bea20d 100644 --- a/pre_commit_hooks/tests_should_end_in_test.py +++ b/pre_commit_hooks/tests_should_end_in_test.py @@ -11,12 +11,12 @@ def validate_files(argv=None): parser.add_argument('filenames', nargs='*') parser.add_argument( '--django', default=False, action='store_true', - help='Use Django-style test naming pattern (test*.py)' + help='Use Django-style test naming pattern (test*.py)', ) args = parser.parse_args(argv) retcode = 0 - test_name_pattern = 'test_.*.py' if args.django else '.*_test.py' + test_name_pattern = 'test.*.py' if args.django else '.*_test.py' for filename in args.filenames: base = basename(filename) if ( @@ -26,9 +26,9 @@ def validate_files(argv=None): ): retcode = 1 print( - '{0} does not match pattern "{1}"'.format( - filename, test_name_pattern - ) + '{} does not match pattern "{}"'.format( + filename, test_name_pattern, + ), ) return retcode diff --git a/pre_commit_hooks/trailing_whitespace_fixer.py b/pre_commit_hooks/trailing_whitespace_fixer.py index d23d58d..062f6e3 100644 --- a/pre_commit_hooks/trailing_whitespace_fixer.py +++ b/pre_commit_hooks/trailing_whitespace_fixer.py @@ -4,8 +4,6 @@ import argparse import os import sys -from pre_commit_hooks.util import cmd_output - def _fix_file(filename, is_markdown): with open(filename, mode='rb') as file_processed: @@ -21,14 +19,19 @@ def _fix_file(filename, is_markdown): def _process_line(line, is_markdown): + if line[-2:] == b'\r\n': + eol = b'\r\n' + elif line[-1:] == b'\n': + eol = b'\n' + else: + eol = b'' # preserve trailing two-space for non-blank lines in markdown files - eol = b'\r\n' if line[-2:] == b'\r\n' else b'\n' if is_markdown and (not line.isspace()) and line.endswith(b' ' + eol): return line.rstrip() + b' ' + eol return line.rstrip() + eol -def fix_trailing_whitespace(argv=None): +def main(argv=None): parser = argparse.ArgumentParser() parser.add_argument( '--no-markdown-linebreak-ext', @@ -36,7 +39,7 @@ def fix_trailing_whitespace(argv=None): const=[], default=argparse.SUPPRESS, dest='markdown_linebreak_ext', - help='Do not preserve linebreak spaces in Markdown' + help='Do not preserve linebreak spaces in Markdown', ) parser.add_argument( '--markdown-linebreak-ext', @@ -45,15 +48,11 @@ def fix_trailing_whitespace(argv=None): default=['md,markdown'], metavar='*|EXT[,EXT,...]', nargs='?', - help='Markdown extensions (or *) for linebreak spaces' + help='Markdown extensions (or *) for linebreak spaces', ) parser.add_argument('filenames', nargs='*', help='Filenames to fix') args = parser.parse_args(argv) - bad_whitespace_files = cmd_output( - 'grep', '-l', '[[:space:]]$', *args.filenames, retcode=None - ).strip().splitlines() - md_args = args.markdown_linebreak_ext if '' in md_args: parser.error('--markdown-linebreak-ext requires a non-empty argument') @@ -67,20 +66,20 @@ def fix_trailing_whitespace(argv=None): for ext in md_exts: if any(c in ext[1:] for c in r'./\:'): parser.error( - "bad --markdown-linebreak-ext extension '{0}' (has . / \\ :)\n" + "bad --markdown-linebreak-ext extension '{}' (has . / \\ :)\n" " (probably filename; use '--markdown-linebreak-ext=EXT')" - .format(ext) + .format(ext), ) return_code = 0 - for bad_whitespace_file in bad_whitespace_files: - _, extension = os.path.splitext(bad_whitespace_file.lower()) + for filename in args.filenames: + _, extension = os.path.splitext(filename.lower()) md = all_markdown or extension in md_exts - if _fix_file(bad_whitespace_file, md): - print('Fixing {}'.format(bad_whitespace_file)) + if _fix_file(filename, md): + print('Fixing {}'.format(filename)) return_code = 1 return return_code if __name__ == '__main__': - sys.exit(fix_trailing_whitespace()) + sys.exit(main()) diff --git a/setup.cfg b/setup.cfg index e57d130..2be6836 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,2 +1,2 @@ -[wheel] +[bdist_wheel] universal = True diff --git a/setup.py b/setup.py index 6851ac1..d59ca7e 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ setup( name='pre_commit_hooks', description='Some out-of-the-box hooks for pre-commit.', url='https://github.com/pre-commit/pre-commit-hooks', - version='0.7.1', + version='1.3.0', author='Anthony Sottile', author_email='asottile@umich.edu', @@ -24,37 +24,44 @@ setup( packages=find_packages(exclude=('tests*', 'testing*')), install_requires=[ - # quickfix to prevent pep8 conflicts - 'flake8==3.3.0', - 'autopep8==1.2.4', + # quickfix to prevent pycodestyle conflicts + 'flake8==3.5.0', + 'autopep8==1.3.5', + 'pycodestyle==2.3.1', 'pyyaml==3.12', - 'simplejson==3.10.0', - 'six==1.10.0', + 'six==1.11.0', ], entry_points={ 'console_scripts': [ 'autopep8-wrapper = pre_commit_hooks.autopep8_wrapper:main', 'check-added-large-files = pre_commit_hooks.check_added_large_files:main', 'check-ast = pre_commit_hooks.check_ast:check_ast', + '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-docstring-first = pre_commit_hooks.check_docstring_first:main', - 'check-json = pre_commit_hooks.check_json:check_json', + 'check-executables-have-shebangs = pre_commit_hooks.check_executables_have_shebangs:main', + 'check-json = pre_commit_check.hooks_json:check_json', 'check-merge-conflict = pre_commit_hooks.check_merge_conflict:detect_merge_conflict', 'check-symlinks = pre_commit_hooks.check_symlinks:check_symlinks', + 'check-vcs-permalinks = pre_commit_hooks.check_vcs_permalinks:main', 'check-xml = pre_commit_hooks.check_xml:check_xml', 'check-yaml = pre_commit_hooks.check_yaml:check_yaml', - 'debug-statement-hook = pre_commit_hooks.debug_statement_hook:debug_statement_hook', + 'debug-statement-hook = pre_commit_hooks.debug_statement_hook:main', 'detect-aws-credentials = pre_commit_hooks.detect_aws_credentials:main', 'detect-private-key = pre_commit_hooks.detect_private_key:detect_private_key', 'double-quote-string-fixer = pre_commit_hooks.string_fixer:main', 'end-of-file-fixer = pre_commit_hooks.end_of_file_fixer:end_of_file_fixer', + 'file-contents-sorter = pre_commit_hooks.file_contents_sorter:main', 'fix-encoding-pragma = pre_commit_hooks.fix_encoding_pragma:main', 'forbid-new-submodules = pre_commit_hooks.forbid_new_submodules:main', + 'mixed-line-ending = pre_commit_hooks.mixed_line_ending:main', 'name-tests-test = pre_commit_hooks.tests_should_end_in_test:validate_files', + 'no-commit-to-branch = pre_commit_hooks.no_commit_to_branch:main', 'pretty-format-json = pre_commit_hooks.pretty_format_json:pretty_format_json', 'requirements-txt-fixer = pre_commit_hooks.requirements_txt_fixer:fix_requirements_txt', - 'trailing-whitespace-fixer = pre_commit_hooks.trailing_whitespace_fixer:fix_trailing_whitespace', + 'sort-simple-yaml = pre_commit_hooks.sort_simple_yaml:main', + 'trailing-whitespace-fixer = pre_commit_hooks.trailing_whitespace_fixer:main', ], }, ) diff --git a/testing/resources/aws_config_with_multiple_sections.ini b/testing/resources/aws_config_with_multiple_sections.ini index ca6a8a3..2053b9a 100644 --- a/testing/resources/aws_config_with_multiple_sections.ini +++ b/testing/resources/aws_config_with_multiple_sections.ini @@ -9,4 +9,4 @@ aws_secret_access_key = z2rpgs5uit782eapz5l1z0y2lurtsyyk6hcfozlb aws_access_key_id = AKIAJIMMINYCRICKET0A aws_secret_access_key = ixswosj8gz3wuik405jl9k3vdajsnxfhnpui38ez [test] -aws_session_token = foo \ No newline at end of file +aws_session_token = foo diff --git a/testing/resources/aws_config_with_secret_and_session_token.ini b/testing/resources/aws_config_with_secret_and_session_token.ini index 4bd675d..4496765 100644 --- a/testing/resources/aws_config_with_secret_and_session_token.ini +++ b/testing/resources/aws_config_with_secret_and_session_token.ini @@ -2,4 +2,4 @@ [production] aws_access_key_id = AKIAVOGONSVOGONS0042 aws_secret_access_key = z2rpgs5uit782eapz5l1z0y2lurtsyyk6hcfozlb -aws_session_token = foo \ No newline at end of file +aws_session_token = foo diff --git a/testing/resources/aws_config_with_session_token.ini b/testing/resources/aws_config_with_session_token.ini index e07f2ac..b03f127 100644 --- a/testing/resources/aws_config_with_session_token.ini +++ b/testing/resources/aws_config_with_session_token.ini @@ -1,3 +1,3 @@ # file with an AWS session token [production] -aws_session_token = foo \ No newline at end of file +aws_session_token = foo diff --git a/testing/resources/aws_config_without_secrets_with_spaces.ini b/testing/resources/aws_config_without_secrets_with_spaces.ini new file mode 100644 index 0000000..b169952 --- /dev/null +++ b/testing/resources/aws_config_without_secrets_with_spaces.ini @@ -0,0 +1,4 @@ +# file with an AWS access key id but no valid AWS secret access key only space characters +[production] +aws_access_key_id = AKIASLARTARGENTINA86 +aws_secret_access_key = diff --git a/testing/resources/broken_symlink b/testing/resources/broken_symlink deleted file mode 120000 index ee1f6cb..0000000 --- a/testing/resources/broken_symlink +++ /dev/null @@ -1 +0,0 @@ -does_not_exist \ No newline at end of file diff --git a/testing/resources/builtin_constructors.py b/testing/resources/builtin_constructors.py new file mode 100644 index 0000000..174a9e8 --- /dev/null +++ b/testing/resources/builtin_constructors.py @@ -0,0 +1,17 @@ +from six.moves import builtins + +c1 = complex() +d1 = dict() +f1 = float() +i1 = int() +l1 = list() +s1 = str() +t1 = tuple() + +c2 = builtins.complex() +d2 = builtins.dict() +f2 = builtins.float() +i2 = builtins.int() +l2 = builtins.list() +s2 = builtins.str() +t2 = builtins.tuple() diff --git a/testing/resources/builtin_literals.py b/testing/resources/builtin_literals.py new file mode 100644 index 0000000..8513b70 --- /dev/null +++ b/testing/resources/builtin_literals.py @@ -0,0 +1,7 @@ +c1 = 0j +d1 = {} +f1 = 0.0 +i1 = 0 +l1 = [] +s1 = '' +t1 = () diff --git a/testing/resources/file_with_debug.notpy b/testing/resources/file_with_debug.notpy deleted file mode 100644 index faa23a2..0000000 --- a/testing/resources/file_with_debug.notpy +++ /dev/null @@ -1,5 +0,0 @@ - -def foo(obj): - import pdb; pdb.set_trace() - - return 5 diff --git a/testing/resources/non_ascii_pretty_formatted_json.json b/testing/resources/non_ascii_pretty_formatted_json.json new file mode 100644 index 0000000..05d0d00 --- /dev/null +++ b/testing/resources/non_ascii_pretty_formatted_json.json @@ -0,0 +1,10 @@ +{ + "alist": [ + 2, + 34, + 234 + ], + "blah": null, + "foo": "bar", + "non_ascii": "中文にほんご한국어" +} diff --git a/testing/resources/working_symlink b/testing/resources/working_symlink deleted file mode 120000 index 20b5061..0000000 --- a/testing/resources/working_symlink +++ /dev/null @@ -1 +0,0 @@ -does_exist \ No newline at end of file diff --git a/testing/util.py b/testing/util.py index 837a3cb..fac498c 100644 --- a/testing/util.py +++ b/testing/util.py @@ -1,7 +1,6 @@ from __future__ import absolute_import from __future__ import unicode_literals -import io import os.path @@ -10,9 +9,3 @@ TESTING_DIR = os.path.abspath(os.path.dirname(__file__)) def get_resource_path(path): return os.path.join(TESTING_DIR, 'resources', path) - - -def write_file(filename, contents): - """Hax because coveragepy chokes on nested context managers.""" - with io.open(filename, 'w', newline='') as file_obj: - file_obj.write(contents) diff --git a/tests/autopep8_wrapper_test.py b/tests/autopep8_wrapper_test.py index 5eb4df2..780752c 100644 --- a/tests/autopep8_wrapper_test.py +++ b/tests/autopep8_wrapper_test.py @@ -23,6 +23,6 @@ def test_main_failing(tmpdir, input_src, expected_ret, output_src): def test_respects_config_file(tmpdir): with tmpdir.as_cwd(): - tmpdir.join('setup.cfg').write('[pep8]\nignore=E221') + tmpdir.join('setup.cfg').write('[pycodestyle]\nignore=E221') tmpdir.join('test.py').write('print(1 + 2)\n') assert main(['test.py', '-i', '-v']) == 0 diff --git a/tests/check_added_large_files_test.py b/tests/check_added_large_files_test.py index ce15f5c..06671d7 100644 --- a/tests/check_added_large_files_test.py +++ b/tests/check_added_large_files_test.py @@ -79,15 +79,6 @@ xfailif_no_gitlfs = pytest.mark.xfail( @xfailif_no_gitlfs def test_allows_gitlfs(temp_git_dir): # pragma: no cover with temp_git_dir.as_cwd(): - # Work around https://github.com/github/git-lfs/issues/913 - cmd_output( - 'git', - 'commit', - '--no-gpg-sign', - '--allow-empty', - '-m', - 'foo', - ) cmd_output('git', 'lfs', 'install') temp_git_dir.join('f.py').write('a' * 10000) cmd_output('git', 'lfs', 'track', 'f.py') diff --git a/tests/check_builtin_literals_test.py b/tests/check_builtin_literals_test.py new file mode 100644 index 0000000..5ab162e --- /dev/null +++ b/tests/check_builtin_literals_test.py @@ -0,0 +1,116 @@ +import ast + +import pytest + +from pre_commit_hooks.check_builtin_literals import BuiltinTypeCall +from pre_commit_hooks.check_builtin_literals import BuiltinTypeVisitor +from pre_commit_hooks.check_builtin_literals import main +from testing.util import get_resource_path + + +@pytest.fixture +def visitor(): + return BuiltinTypeVisitor() + + +@pytest.mark.parametrize( + ('expression', 'calls'), + [ + # see #285 + ('x[0]()', []), + # complex + ("0j", []), + ("complex()", [BuiltinTypeCall('complex', 1, 0)]), + ("complex(0, 0)", []), + ("complex('0+0j')", []), + ('builtins.complex()', []), + # float + ("0.0", []), + ("float()", [BuiltinTypeCall('float', 1, 0)]), + ("float('0.0')", []), + ('builtins.float()', []), + # int + ("0", []), + ("int()", [BuiltinTypeCall('int', 1, 0)]), + ("int('0')", []), + ('builtins.int()', []), + # list + ("[]", []), + ("list()", [BuiltinTypeCall('list', 1, 0)]), + ("list('abc')", []), + ("list([c for c in 'abc'])", []), + ("list(c for c in 'abc')", []), + ('builtins.list()', []), + # str + ("''", []), + ("str()", [BuiltinTypeCall('str', 1, 0)]), + ("str('0')", []), + ('builtins.str()', []), + # tuple + ("()", []), + ("tuple()", [BuiltinTypeCall('tuple', 1, 0)]), + ("tuple('abc')", []), + ("tuple([c for c in 'abc'])", []), + ("tuple(c for c in 'abc')", []), + ('builtins.tuple()', []), + ], +) +def test_non_dict_exprs(visitor, expression, calls): + visitor.visit(ast.parse(expression)) + assert visitor.builtin_type_calls == calls + + +@pytest.mark.parametrize( + ('expression', 'calls'), + [ + ("{}", []), + ("dict()", [BuiltinTypeCall('dict', 1, 0)]), + ("dict(a=1, b=2, c=3)", []), + ("dict(**{'a': 1, 'b': 2, 'c': 3})", []), + ("dict([(k, v) for k, v in [('a', 1), ('b', 2), ('c', 3)]])", []), + ("dict((k, v) for k, v in [('a', 1), ('b', 2), ('c', 3)])", []), + ('builtins.dict()', []), + ], +) +def test_dict_allow_kwargs_exprs(visitor, expression, calls): + visitor.visit(ast.parse(expression)) + assert visitor.builtin_type_calls == calls + + +@pytest.mark.parametrize( + ('expression', 'calls'), + [ + ("dict()", [BuiltinTypeCall('dict', 1, 0)]), + ("dict(a=1, b=2, c=3)", [BuiltinTypeCall('dict', 1, 0)]), + ("dict(**{'a': 1, 'b': 2, 'c': 3})", [BuiltinTypeCall('dict', 1, 0)]), + ('builtins.dict()', []), + ], +) +def test_dict_no_allow_kwargs_exprs(expression, calls): + visitor = BuiltinTypeVisitor(allow_dict_kwargs=False) + visitor.visit(ast.parse(expression)) + assert visitor.builtin_type_calls == calls + + +def test_ignore_constructors(): + visitor = BuiltinTypeVisitor(ignore=('complex', 'dict', 'float', 'int', 'list', 'str', 'tuple')) + visitor.visit(ast.parse(open(get_resource_path('builtin_constructors.py'), 'rb').read(), 'builtin_constructors.py')) + assert visitor.builtin_type_calls == [] + + +def test_failing_file(): + rc = main([get_resource_path('builtin_constructors.py')]) + assert rc == 1 + + +def test_passing_file(): + rc = main([get_resource_path('builtin_literals.py')]) + assert rc == 0 + + +def test_failing_file_ignore_all(): + rc = main([ + '--ignore=complex,dict,float,int,list,str,tuple', + get_resource_path('builtin_constructors.py'), + ]) + assert rc == 0 diff --git a/tests/check_docstring_first_test.py b/tests/check_docstring_first_test.py index f14880b..aa9898d 100644 --- a/tests/check_docstring_first_test.py +++ b/tests/check_docstring_first_test.py @@ -19,7 +19,7 @@ TESTS = ( '"foo"\n', 1, '{filename}:2 Module docstring appears after code ' - '(code seen on line 1).\n' + '(code seen on line 1).\n', ), # Test double docstring ( @@ -28,7 +28,7 @@ TESTS = ( '"fake docstring"\n', 1, '{filename}:3 Multiple module docstrings ' - '(first docstring on line 1).\n' + '(first docstring on line 1).\n', ), # Test multiple lines of code above ( diff --git a/tests/check_executables_have_shebangs_test.py b/tests/check_executables_have_shebangs_test.py new file mode 100644 index 0000000..0cb9dcf --- /dev/null +++ b/tests/check_executables_have_shebangs_test.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import +from __future__ import unicode_literals + +import pytest + +from pre_commit_hooks.check_executables_have_shebangs import main + + +@pytest.mark.parametrize( + 'content', ( + b'#!/bin/bash\nhello world\n', + b'#!/usr/bin/env python3.6', + b'#!python', + '#!☃'.encode('UTF-8'), + ), +) +def test_has_shebang(content, tmpdir): + path = tmpdir.join('path') + path.write(content, 'wb') + assert main((path.strpath,)) == 0 + + +@pytest.mark.parametrize( + 'content', ( + b'', + b' #!python\n', + b'\n#!python\n', + b'python\n', + '☃'.encode('UTF-8'), + + ), +) +def test_bad_shebang(content, tmpdir, capsys): + path = tmpdir.join('path') + path.write(content, 'wb') + assert main((path.strpath,)) == 1 + _, stderr = capsys.readouterr() + assert stderr.startswith('{}: marked executable but'.format(path.strpath)) diff --git a/tests/check_json_test.py b/tests/check_json_test.py index 996bfbb..6ba26c1 100644 --- a/tests/check_json_test.py +++ b/tests/check_json_test.py @@ -4,11 +4,13 @@ from pre_commit_hooks.check_json import check_json from testing.util import get_resource_path -@pytest.mark.parametrize(('filename', 'expected_retval'), ( - ('bad_json.notjson', 1), - ('bad_json_latin1.nonjson', 1), - ('ok_json.json', 0), -)) +@pytest.mark.parametrize( + ('filename', 'expected_retval'), ( + ('bad_json.notjson', 1), + ('bad_json_latin1.nonjson', 1), + ('ok_json.json', 0), + ), +) def test_check_json(capsys, filename, expected_retval): ret = check_json([get_resource_path(filename)]) assert ret == expected_retval diff --git a/tests/check_merge_conflict_test.py b/tests/check_merge_conflict_test.py index f1528b2..1045174 100644 --- a/tests/check_merge_conflict_test.py +++ b/tests/check_merge_conflict_test.py @@ -9,10 +9,9 @@ import pytest from pre_commit_hooks.check_merge_conflict import detect_merge_conflict from pre_commit_hooks.util import cmd_output from testing.util import get_resource_path -from testing.util import write_file -@pytest.yield_fixture +@pytest.fixture def f1_is_a_conflict_file(tmpdir): # Make a merge conflict repo1 = tmpdir.join('repo1') @@ -37,7 +36,7 @@ def f1_is_a_conflict_file(tmpdir): with repo2.as_cwd(): repo2_f1.write('child\n') cmd_output('git', 'commit', '--no-gpg-sign', '-am', 'clone commit2') - cmd_output('git', 'pull', retcode=None) + cmd_output('git', 'pull', '--no-rebase', retcode=None) # We should end up in a merge conflict! f1 = repo2_f1.read() assert f1.startswith( @@ -45,7 +44,7 @@ def f1_is_a_conflict_file(tmpdir): 'child\n' '=======\n' 'parent\n' - '>>>>>>>' + '>>>>>>>', ) or f1.startswith( '<<<<<<< HEAD\n' 'child\n' @@ -53,14 +52,22 @@ def f1_is_a_conflict_file(tmpdir): '||||||| merged common ancestors\n' '=======\n' 'parent\n' - '>>>>>>>' + '>>>>>>>', + ) or f1.startswith( + # .gitconfig with [pull] rebase = preserve causes a rebase which + # flips parent / child + '<<<<<<< HEAD\n' + 'parent\n' + '=======\n' + 'child\n' + '>>>>>>>', ) assert os.path.exists(os.path.join('.git', 'MERGE_MSG')) - yield + yield repo2 -@pytest.yield_fixture -def repository_is_pending_merge(tmpdir): +@pytest.fixture +def repository_pending_merge(tmpdir): # Make a (non-conflicting) merge repo1 = tmpdir.join('repo1') repo1_f1 = repo1.join('f1') @@ -85,12 +92,12 @@ def repository_is_pending_merge(tmpdir): repo2_f2.write('child\n') cmd_output('git', 'add', '--', repo2_f2.strpath) cmd_output('git', 'commit', '--no-gpg-sign', '-m', 'clone commit2') - cmd_output('git', 'pull', '--no-commit') + cmd_output('git', 'pull', '--no-commit', '--no-rebase') # We should end up in a pending merge assert repo2_f1.read() == 'parent\n' assert repo2_f2.read() == 'child\n' assert os.path.exists(os.path.join('.git', 'MERGE_HEAD')) - yield + yield repo2 @pytest.mark.usefixtures('f1_is_a_conflict_file') @@ -99,20 +106,18 @@ def test_merge_conflicts_git(): @pytest.mark.parametrize( - 'failing_contents', ('<<<<<<< HEAD\n', '=======\n', '>>>>>>> master\n'), + 'contents', (b'<<<<<<< HEAD\n', b'=======\n', b'>>>>>>> master\n'), ) -@pytest.mark.usefixtures('repository_is_pending_merge') -def test_merge_conflicts_failing(failing_contents): - write_file('f2', failing_contents) +def test_merge_conflicts_failing(contents, repository_pending_merge): + repository_pending_merge.join('f2').write_binary(contents) assert detect_merge_conflict(['f2']) == 1 @pytest.mark.parametrize( - 'ok_contents', ('# <<<<<<< HEAD\n', '# =======\n', 'import my_module', ''), + 'contents', (b'# <<<<<<< HEAD\n', b'# =======\n', b'import mod', b''), ) -@pytest.mark.usefixtures('f1_is_a_conflict_file') -def test_merge_conflicts_ok(ok_contents): - write_file('f1', ok_contents) +def test_merge_conflicts_ok(contents, f1_is_a_conflict_file): + f1_is_a_conflict_file.join('f1').write_binary(contents) assert detect_merge_conflict(['f1']) == 0 diff --git a/tests/check_symlinks_test.py b/tests/check_symlinks_test.py index 4b11e71..0414df5 100644 --- a/tests/check_symlinks_test.py +++ b/tests/check_symlinks_test.py @@ -3,14 +3,21 @@ import os import pytest from pre_commit_hooks.check_symlinks import check_symlinks -from testing.util import get_resource_path -@pytest.mark.xfail(os.name == 'nt', reason='No symlink support on windows') -@pytest.mark.parametrize(('filename', 'expected_retval'), ( - ('broken_symlink', 1), - ('working_symlink', 0), -)) -def test_check_symlinks(filename, expected_retval): - ret = check_symlinks([get_resource_path(filename)]) - assert ret == expected_retval +xfail_symlink = pytest.mark.xfail(os.name == 'nt', reason='No symlink support') + + +@xfail_symlink +@pytest.mark.parametrize( + ('dest', 'expected'), (('exists', 0), ('does-not-exist', 1)), +) +def test_check_symlinks(tmpdir, dest, expected): # pragma: no cover (symlinks) + tmpdir.join('exists').ensure() + symlink = tmpdir.join('symlink') + symlink.mksymlinkto(tmpdir.join(dest)) + assert check_symlinks((symlink.strpath,)) == expected + + +def test_check_symlinks_normal_file(tmpdir): + assert check_symlinks((tmpdir.join('f').ensure().strpath,)) == 0 diff --git a/tests/check_vcs_permalinks_test.py b/tests/check_vcs_permalinks_test.py new file mode 100644 index 0000000..00e5396 --- /dev/null +++ b/tests/check_vcs_permalinks_test.py @@ -0,0 +1,38 @@ +from __future__ import absolute_import +from __future__ import unicode_literals + +from pre_commit_hooks.check_vcs_permalinks import main + + +def test_trivial(tmpdir): + f = tmpdir.join('f.txt').ensure() + assert not main((f.strpath,)) + + +def test_passing(tmpdir): + f = tmpdir.join('f.txt') + f.write_binary( + # permalinks are ok + b'https://github.com/asottile/test/blob/649e6/foo%20bar#L1\n' + # links to files but not line numbers are ok + b'https://github.com/asottile/test/blob/master/foo%20bar\n' + # regression test for overly-greedy regex + b'https://github.com/ yes / no ? /blob/master/foo#L1\n', + ) + assert not main((f.strpath,)) + + +def test_failing(tmpdir, capsys): + with tmpdir.as_cwd(): + tmpdir.join('f.txt').write_binary( + b'https://github.com/asottile/test/blob/master/foo#L1\n', + ) + + assert main(('f.txt',)) + out, _ = capsys.readouterr() + assert out == ( + 'f.txt:1:https://github.com/asottile/test/blob/master/foo#L1\n' + '\n' + 'Non-permanent github link detected.\n' + 'On any page on github press [y] to load a permalink.\n' + ) diff --git a/tests/check_xml_test.py b/tests/check_xml_test.py index 99b0902..84e365d 100644 --- a/tests/check_xml_test.py +++ b/tests/check_xml_test.py @@ -4,10 +4,12 @@ from pre_commit_hooks.check_xml import check_xml from testing.util import get_resource_path -@pytest.mark.parametrize(('filename', 'expected_retval'), ( - ('bad_xml.notxml', 1), - ('ok_xml.xml', 0), -)) +@pytest.mark.parametrize( + ('filename', 'expected_retval'), ( + ('bad_xml.notxml', 1), + ('ok_xml.xml', 0), + ), +) def test_check_xml(filename, expected_retval): ret = check_xml([get_resource_path(filename)]) assert ret == expected_retval diff --git a/tests/check_yaml_test.py b/tests/check_yaml_test.py index c145fdc..aa357f1 100644 --- a/tests/check_yaml_test.py +++ b/tests/check_yaml_test.py @@ -1,13 +1,54 @@ +from __future__ import absolute_import +from __future__ import unicode_literals + import pytest from pre_commit_hooks.check_yaml import check_yaml from testing.util import get_resource_path -@pytest.mark.parametrize(('filename', 'expected_retval'), ( - ('bad_yaml.notyaml', 1), - ('ok_yaml.yaml', 0), -)) +@pytest.mark.parametrize( + ('filename', 'expected_retval'), ( + ('bad_yaml.notyaml', 1), + ('ok_yaml.yaml', 0), + ), +) def test_check_yaml(filename, expected_retval): ret = check_yaml([get_resource_path(filename)]) assert ret == expected_retval + + +def test_check_yaml_allow_multiple_documents(tmpdir): + f = tmpdir.join('test.yaml') + f.write('---\nfoo\n---\nbar\n') + + # should fail without the setting + assert check_yaml((f.strpath,)) + + # should pass when we allow multiple documents + assert not check_yaml(('--allow-multiple-documents', f.strpath)) + + +def test_fails_even_with_allow_multiple_documents(tmpdir): + f = tmpdir.join('test.yaml') + f.write('[') + assert check_yaml(('--allow-multiple-documents', f.strpath)) + + +def test_check_yaml_unsafe(tmpdir): + f = tmpdir.join('test.yaml') + f.write( + 'some_foo: !vault |\n' + ' $ANSIBLE_VAULT;1.1;AES256\n' + ' deadbeefdeadbeefdeadbeef\n', + ) + # should fail "safe" check + assert check_yaml((f.strpath,)) + # should pass when we allow unsafe documents + assert not check_yaml(('--unsafe', f.strpath)) + + +def test_check_yaml_unsafe_still_fails_on_syntax_errors(tmpdir): + f = tmpdir.join('test.yaml') + f.write('[') + assert check_yaml(('--unsafe', f.strpath)) diff --git a/tests/conftest.py b/tests/conftest.py index 87fec70..da206cb 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,7 +7,7 @@ import pytest from pre_commit_hooks.util import cmd_output -@pytest.yield_fixture +@pytest.fixture def temp_git_dir(tmpdir): git_dir = tmpdir.join('gits') cmd_output('git', 'init', '--', git_dir.strpath) diff --git a/tests/debug_statement_hook_test.py b/tests/debug_statement_hook_test.py index c318346..d15f5f7 100644 --- a/tests/debug_statement_hook_test.py +++ b/tests/debug_statement_hook_test.py @@ -1,73 +1,63 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import +from __future__ import unicode_literals + import ast -import pytest - -from pre_commit_hooks.debug_statement_hook import debug_statement_hook -from pre_commit_hooks.debug_statement_hook import DebugStatement -from pre_commit_hooks.debug_statement_hook import ImportStatementParser +from pre_commit_hooks.debug_statement_hook import Debug +from pre_commit_hooks.debug_statement_hook import DebugStatementParser +from pre_commit_hooks.debug_statement_hook import main from testing.util import get_resource_path -@pytest.fixture -def ast_with_no_debug_imports(): - return ast.parse(""" -import foo -import bar -import baz -from foo import bar -""") +def test_no_breakpoints(): + visitor = DebugStatementParser() + visitor.visit(ast.parse('import os\nfrom foo import bar\n')) + assert visitor.breakpoints == [] -@pytest.fixture -def ast_with_debug_import_form_1(): - return ast.parse(""" - -import ipdb; ipdb.set_trace() - -""") +def test_finds_debug_import_attribute_access(): + visitor = DebugStatementParser() + visitor.visit(ast.parse('import ipdb; ipdb.set_trace()')) + assert visitor.breakpoints == [Debug(1, 0, 'ipdb', 'imported')] -@pytest.fixture -def ast_with_debug_import_form_2(): - return ast.parse(""" - -from pudb import set_trace; set_trace() - -""") +def test_finds_debug_import_from_import(): + visitor = DebugStatementParser() + visitor.visit(ast.parse('from pudb import set_trace; set_trace()')) + assert visitor.breakpoints == [Debug(1, 0, 'pudb', 'imported')] -def test_returns_no_debug_statements(ast_with_no_debug_imports): - visitor = ImportStatementParser() - visitor.visit(ast_with_no_debug_imports) - assert visitor.debug_import_statements == [] +def test_finds_breakpoint(): + visitor = DebugStatementParser() + visitor.visit(ast.parse('breakpoint()')) + assert visitor.breakpoints == [Debug(1, 0, 'breakpoint', 'called')] -def test_returns_one_form_1(ast_with_debug_import_form_1): - visitor = ImportStatementParser() - visitor.visit(ast_with_debug_import_form_1) - assert visitor.debug_import_statements == [ - DebugStatement('ipdb', 3, 0) - ] - - -def test_returns_one_form_2(ast_with_debug_import_form_2): - visitor = ImportStatementParser() - visitor.visit(ast_with_debug_import_form_2) - assert visitor.debug_import_statements == [ - DebugStatement('pudb', 3, 0) - ] - - -def test_returns_one_for_failing_file(): - ret = debug_statement_hook([get_resource_path('file_with_debug.notpy')]) +def test_returns_one_for_failing_file(tmpdir): + f_py = tmpdir.join('f.py') + f_py.write('def f():\n import pdb; pdb.set_trace()') + ret = main([f_py.strpath]) assert ret == 1 def test_returns_zero_for_passing_file(): - ret = debug_statement_hook([__file__]) + ret = main([__file__]) assert ret == 0 def test_syntaxerror_file(): - ret = debug_statement_hook([get_resource_path('cannot_parse_ast.notpy')]) + ret = main([get_resource_path('cannot_parse_ast.notpy')]) assert ret == 1 + + +def test_non_utf8_file(tmpdir): + f_py = tmpdir.join('f.py') + f_py.write_binary('# -*- coding: cp1252 -*-\nx = "€"\n'.encode('cp1252')) + assert main((f_py.strpath,)) == 0 + + +def test_py37_breakpoint(tmpdir): + f_py = tmpdir.join('f.py') + f_py.write('def f():\n breakpoint()\n') + assert main((f_py.strpath,)) == 1 diff --git a/tests/detect_aws_credentials_test.py b/tests/detect_aws_credentials_test.py index 943a3f8..f1bd7d4 100644 --- a/tests/detect_aws_credentials_test.py +++ b/tests/detect_aws_credentials_test.py @@ -21,16 +21,16 @@ from testing.util import get_resource_path ( { 'AWS_DUMMY_KEY': '/foo', 'AWS_CONFIG_FILE': '/bar', - 'AWS_CREDENTIAL_FILE': '/baz' + 'AWS_CREDENTIAL_FILE': '/baz', }, - {'/bar', '/baz'} + {'/bar', '/baz'}, ), ( { 'AWS_CONFIG_FILE': '/foo', 'AWS_CREDENTIAL_FILE': '/bar', - 'AWS_SHARED_CREDENTIALS_FILE': '/baz' + 'AWS_SHARED_CREDENTIALS_FILE': '/baz', }, - {'/foo', '/bar', '/baz'} + {'/foo', '/bar', '/baz'}, ), ), ) @@ -51,7 +51,7 @@ def test_get_aws_credentials_file_from_env(env_vars, values): ({'AWS_DUMMY_KEY': 'foo', 'AWS_SECRET_ACCESS_KEY': 'bar'}, {'bar'}), ( {'AWS_SECRET_ACCESS_KEY': 'foo', 'AWS_SECURITY_TOKEN': 'bar'}, - {'foo', 'bar'} + {'foo', 'bar'}, ), ), ) @@ -66,21 +66,24 @@ def test_get_aws_secrets_from_env(env_vars, values): ( ( 'aws_config_with_secret.ini', - {'z2rpgs5uit782eapz5l1z0y2lurtsyyk6hcfozlb'} + {'z2rpgs5uit782eapz5l1z0y2lurtsyyk6hcfozlb'}, ), ('aws_config_with_session_token.ini', {'foo'}), - ('aws_config_with_secret_and_session_token.ini', - {'z2rpgs5uit782eapz5l1z0y2lurtsyyk6hcfozlb', 'foo'}), + ( + 'aws_config_with_secret_and_session_token.ini', + {'z2rpgs5uit782eapz5l1z0y2lurtsyyk6hcfozlb', 'foo'}, + ), ( 'aws_config_with_multiple_sections.ini', { '7xebzorgm5143ouge9gvepxb2z70bsb2rtrh099e', 'z2rpgs5uit782eapz5l1z0y2lurtsyyk6hcfozlb', 'ixswosj8gz3wuik405jl9k3vdajsnxfhnpui38ez', - 'foo' - } + 'foo', + }, ), ('aws_config_without_secrets.ini', set()), + ('aws_config_without_secrets_with_spaces.ini', set()), ('nonsense.txt', set()), ('ok_json.json', set()), ), @@ -98,6 +101,7 @@ def test_get_aws_secrets_from_file(filename, expected_keys): ('aws_config_with_session_token.ini', 1), ('aws_config_with_multiple_sections.ini', 1), ('aws_config_without_secrets.ini', 0), + ('aws_config_without_secrets_with_spaces.ini', 0), ('nonsense.txt', 0), ('ok_json.json', 0), ), @@ -121,7 +125,7 @@ def test_non_existent_credentials(mock_secrets_env, mock_secrets_file, capsys): mock_secrets_file.return_value = set() ret = main(( get_resource_path('aws_config_without_secrets.ini'), - "--credentials-file=testing/resources/credentailsfilethatdoesntexist" + "--credentials-file=testing/resources/credentailsfilethatdoesntexist", )) assert ret == 2 out, _ = capsys.readouterr() @@ -141,6 +145,6 @@ def test_non_existent_credentials_with_allow_flag(mock_secrets_env, mock_secrets ret = main(( get_resource_path('aws_config_without_secrets.ini'), "--credentials-file=testing/resources/credentailsfilethatdoesntexist", - "--allow-missing-credentials" + "--allow-missing-credentials", )) assert ret == 0 diff --git a/tests/detect_private_key_test.py b/tests/detect_private_key_test.py index c6558ba..fdd63a2 100644 --- a/tests/detect_private_key_test.py +++ b/tests/detect_private_key_test.py @@ -8,6 +8,8 @@ TESTS = ( (b'-----BEGIN DSA PRIVATE KEY-----', 1), (b'-----BEGIN EC PRIVATE KEY-----', 1), (b'-----BEGIN OPENSSH PRIVATE KEY-----', 1), + (b'PuTTY-User-Key-File-2: ssh-rsa', 1), + (b'---- BEGIN SSH2 ENCRYPTED PRIVATE KEY ----', 1), (b'ssh-rsa DATA', 0), (b'ssh-dsa DATA', 0), # Some arbitrary binary data diff --git a/tests/file_contents_sorter_test.py b/tests/file_contents_sorter_test.py new file mode 100644 index 0000000..1f9a14b --- /dev/null +++ b/tests/file_contents_sorter_test.py @@ -0,0 +1,33 @@ +import pytest + +from pre_commit_hooks.file_contents_sorter import FAIL +from pre_commit_hooks.file_contents_sorter import main +from pre_commit_hooks.file_contents_sorter import PASS + + +@pytest.mark.parametrize( + ('input_s', 'expected_retval', 'output'), + ( + (b'', FAIL, b'\n'), + (b'lonesome\n', PASS, b'lonesome\n'), + (b'missing_newline', FAIL, b'missing_newline\n'), + (b'newline\nmissing', FAIL, b'missing\nnewline\n'), + (b'missing\nnewline', FAIL, b'missing\nnewline\n'), + (b'alpha\nbeta\n', PASS, b'alpha\nbeta\n'), + (b'beta\nalpha\n', FAIL, b'alpha\nbeta\n'), + (b'C\nc\n', PASS, b'C\nc\n'), + (b'c\nC\n', FAIL, b'C\nc\n'), + (b'mag ical \n tre vor\n', FAIL, b' tre vor\nmag ical \n'), + (b'@\n-\n_\n#\n', FAIL, b'#\n-\n@\n_\n'), + (b'extra\n\n\nwhitespace\n', FAIL, b'extra\nwhitespace\n'), + (b'whitespace\n\n\nextra\n', FAIL, b'extra\nwhitespace\n'), + ), +) +def test_integration(input_s, expected_retval, output, tmpdir): + path = tmpdir.join('file.txt') + path.write_binary(input_s) + + output_retval = main([path.strpath]) + + assert path.read_binary() == output + assert output_retval == expected_retval diff --git a/tests/fix_encoding_pragma_test.py b/tests/fix_encoding_pragma_test.py index d49f1ba..7288bfa 100644 --- a/tests/fix_encoding_pragma_test.py +++ b/tests/fix_encoding_pragma_test.py @@ -56,7 +56,7 @@ def test_integration_remove_ok(tmpdir): b'# -*- coding: utf-8 -*-\n' b'foo = "bar"\n' ), - ) + ), ) def test_ok_inputs(input_str): bytesio = io.BytesIO(input_str) @@ -100,7 +100,7 @@ def test_ok_inputs(input_str): (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) diff --git a/tests/forbid_new_submodules_test.py b/tests/forbid_new_submodules_test.py index 1750e00..dc64f06 100644 --- a/tests/forbid_new_submodules_test.py +++ b/tests/forbid_new_submodules_test.py @@ -6,14 +6,18 @@ from pre_commit.util import cmd_output from pre_commit_hooks.forbid_new_submodules import main -@pytest.yield_fixture +@pytest.fixture def git_dir_with_git_dir(tmpdir): with tmpdir.as_cwd(): cmd_output('git', 'init', '.') - cmd_output('git', 'commit', '-m', 'init', '--allow-empty') + cmd_output( + 'git', 'commit', '-m', 'init', '--allow-empty', '--no-gpg-sign', + ) cmd_output('git', 'init', 'foo') - with tmpdir.join('foo').as_cwd(): - cmd_output('git', 'commit', '-m', 'init', '--allow-empty') + cmd_output( + 'git', 'commit', '-m', 'init', '--allow-empty', '--no-gpg-sign', + cwd=tmpdir.join('foo').strpath, + ) yield diff --git a/tests/meta_test.py b/tests/meta_test.py index 202b3d7..e5d068f 100644 --- a/tests/meta_test.py +++ b/tests/meta_test.py @@ -1,7 +1,43 @@ +from __future__ import absolute_import +from __future__ import unicode_literals + import io +import yaml -def test_hooks_yaml_same_contents(): - legacy_contents = io.open('hooks.yaml').read() - contents = io.open('.pre-commit-hooks.yaml').read() - assert legacy_contents == contents + +def _assert_parseable_in_old_pre_commit(hooks): + for hook in hooks: + assert {'id', 'name', 'entry', 'files', 'language'} <= set(hook) + + +def test_legacy_hooks(): + with io.open('hooks.yaml', encoding='UTF-8') as legacy_file: + legacy = yaml.load(legacy_file.read()) + with io.open('.pre-commit-hooks.yaml', encoding='UTF-8') as hooks_file: + hooks = yaml.load(hooks_file.read()) + + # The same set of hooks should be defined in both files + new_hook_ids = {hook['id'] for hook in hooks} + legacy_hook_ids = {hook['id'] for hook in legacy} + assert new_hook_ids == legacy_hook_ids + + # Both files should be parseable by pre-commit<0.15.0 + _assert_parseable_in_old_pre_commit(legacy) + _assert_parseable_in_old_pre_commit(hooks) + + # The legacy file should force upgrading + for hook in legacy: + del hook['id'] + assert hook == { + 'language': 'system', + 'name': 'upgrade-your-pre-commit-version', + 'entry': 'upgrade-your-pre-commit-version', + 'files': '', + 'minimum_pre_commit_version': '0.15.0', + } + + # Each hook should require a new version if it uses types + for hook in hooks: + if 'types' in hook: + assert hook['minimum_pre_commit_version'] == '0.15.0' diff --git a/tests/mixed_line_ending_test.py b/tests/mixed_line_ending_test.py new file mode 100644 index 0000000..23837cd --- /dev/null +++ b/tests/mixed_line_ending_test.py @@ -0,0 +1,113 @@ +from __future__ import absolute_import +from __future__ import unicode_literals + +import pytest + +from pre_commit_hooks.mixed_line_ending import main + + +@pytest.mark.parametrize( + ('input_s', 'output'), + ( + # mixed with majority of 'LF' + (b'foo\r\nbar\nbaz\n', b'foo\nbar\nbaz\n'), + # mixed with majority of 'CRLF' + (b'foo\r\nbar\nbaz\r\n', b'foo\r\nbar\r\nbaz\r\n'), + # mixed with majority of 'CR' + (b'foo\rbar\nbaz\r', b'foo\rbar\rbaz\r'), + # mixed with as much 'LF' as 'CRLF' + (b'foo\r\nbar\n', b'foo\nbar\n'), + # mixed with as much 'LF' as 'CR' + (b'foo\rbar\n', b'foo\nbar\n'), + # mixed with as much 'CRLF' as 'CR' + (b'foo\r\nbar\r', b'foo\r\nbar\r\n'), + # mixed with as much 'CRLF' as 'LF' as 'CR' + (b'foo\r\nbar\nbaz\r', b'foo\nbar\nbaz\n'), + ), +) +def test_mixed_line_ending_fixes_auto(input_s, output, tmpdir): + path = tmpdir.join('file.txt') + path.write_binary(input_s) + ret = main((path.strpath,)) + + assert ret == 1 + assert path.read_binary() == output + + +def test_non_mixed_no_newline_end_of_file(tmpdir): + path = tmpdir.join('f.txt') + path.write_binary(b'foo\nbar\nbaz') + assert not main((path.strpath,)) + # the hook *could* fix the end of the file, but leaves it alone + # this is mostly to document the current behaviour + assert path.read_binary() == b'foo\nbar\nbaz' + + +def test_mixed_no_newline_end_of_file(tmpdir): + path = tmpdir.join('f.txt') + path.write_binary(b'foo\r\nbar\nbaz') + assert main((path.strpath,)) + # 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 + # this is mostly to document the current behaviour + assert path.read_binary() == b'foo\nbar\nbaz\n' + + +@pytest.mark.parametrize( + ('fix_option', 'input_s'), + ( + # All --fix=auto with uniform line endings should be ok + ('--fix=auto', b'foo\r\nbar\r\nbaz\r\n'), + ('--fix=auto', b'foo\rbar\rbaz\r'), + ('--fix=auto', b'foo\nbar\nbaz\n'), + # --fix=crlf with crlf endings + ('--fix=crlf', b'foo\r\nbar\r\nbaz\r\n'), + # --fix=lf with lf endings + ('--fix=lf', b'foo\nbar\nbaz\n'), + ), +) +def test_line_endings_ok(fix_option, input_s, tmpdir): + path = tmpdir.join('input.txt') + path.write_binary(input_s) + ret = main((fix_option, path.strpath)) + + assert ret == 0 + assert path.read_binary() == input_s + + +def test_no_fix_does_not_modify(tmpdir): + path = tmpdir.join('input.txt') + contents = b'foo\r\nbar\rbaz\nwomp\n' + path.write_binary(contents) + ret = main(('--fix=no', path.strpath)) + + assert ret == 1 + assert path.read_binary() == contents + + +def test_fix_lf(tmpdir): + path = tmpdir.join('input.txt') + path.write_binary(b'foo\r\nbar\rbaz\n') + ret = main(('--fix=lf', path.strpath)) + + assert ret == 1 + assert path.read_binary() == b'foo\nbar\nbaz\n' + + +def test_fix_crlf(tmpdir): + path = tmpdir.join('input.txt') + path.write_binary(b'foo\r\nbar\rbaz\n') + ret = main(('--fix=crlf', path.strpath)) + + assert ret == 1 + assert path.read_binary() == b'foo\r\nbar\r\nbaz\r\n' + + +def test_fix_lf_all_crlf(tmpdir): + """Regression test for #239""" + path = tmpdir.join('input.txt') + path.write_binary(b'foo\r\nbar\r\n') + ret = main(('--fix=lf', path.strpath)) + + assert ret == 1 + assert path.read_binary() == b'foo\nbar\n' diff --git a/tests/no_commit_to_branch_test.py b/tests/no_commit_to_branch_test.py new file mode 100644 index 0000000..c275bf7 --- /dev/null +++ b/tests/no_commit_to_branch_test.py @@ -0,0 +1,59 @@ +from __future__ import absolute_import +from __future__ import unicode_literals + +import pytest + +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.util import cmd_output + + +def test_other_branch(temp_git_dir): + with temp_git_dir.as_cwd(): + cmd_output('git', 'checkout', '-b', 'anotherbranch') + assert is_on_branch(('master',)) is False + + +def test_multi_branch(temp_git_dir): + with temp_git_dir.as_cwd(): + cmd_output('git', 'checkout', '-b', 'another/branch') + assert is_on_branch(('master',)) is False + + +def test_multi_branch_fail(temp_git_dir): + with temp_git_dir.as_cwd(): + cmd_output('git', 'checkout', '-b', 'another/branch') + assert is_on_branch(('another/branch',)) is True + + +def test_master_branch(temp_git_dir): + with temp_git_dir.as_cwd(): + assert is_on_branch(('master',)) is True + + +def test_main_branch_call(temp_git_dir): + with temp_git_dir.as_cwd(): + cmd_output('git', 'checkout', '-b', 'other') + assert main(('--branch', 'other')) == 1 + + +@pytest.mark.parametrize('branch_name', ('b1', 'b2')) +def test_forbid_multiple_branches(temp_git_dir, branch_name): + with temp_git_dir.as_cwd(): + cmd_output('git', 'checkout', '-b', branch_name) + assert main(('--branch', 'b1', '--branch', 'b2')) + + +def test_main_default_call(temp_git_dir): + with temp_git_dir.as_cwd(): + cmd_output('git', 'checkout', '-b', 'anotherbranch') + assert main(()) == 0 + + +def test_not_on_a_branch(temp_git_dir): + with temp_git_dir.as_cwd(): + cmd_output('git', 'commit', '--no-gpg-sign', '--allow-empty', '-m1') + head = cmd_output('git', 'rev-parse', 'HEAD').strip() + cmd_output('git', 'checkout', head) + # we're not on a branch! + assert main(()) == 0 diff --git a/tests/pretty_format_json_test.py b/tests/pretty_format_json_test.py index 7bfc31f..7ce7e16 100644 --- a/tests/pretty_format_json_test.py +++ b/tests/pretty_format_json_test.py @@ -1,53 +1,66 @@ import shutil import pytest +from six import PY2 -from pre_commit_hooks.pretty_format_json import parse_indent +from pre_commit_hooks.pretty_format_json import parse_num_to_int from pre_commit_hooks.pretty_format_json import pretty_format_json from testing.util import get_resource_path -def test_parse_indent(): - assert parse_indent('0') == '' - assert parse_indent('2') == ' ' - assert parse_indent('\t') == '\t' - with pytest.raises(ValueError): - parse_indent('a') - with pytest.raises(ValueError): - parse_indent('-2') +def test_parse_num_to_int(): + assert parse_num_to_int('0') == 0 + assert parse_num_to_int('2') == 2 + assert parse_num_to_int('\t') == '\t' + assert parse_num_to_int(' ') == ' ' -@pytest.mark.parametrize(('filename', 'expected_retval'), ( - ('not_pretty_formatted_json.json', 1), - ('unsorted_pretty_formatted_json.json', 1), - ('pretty_formatted_json.json', 0), -)) +@pytest.mark.parametrize( + ('filename', 'expected_retval'), ( + ('not_pretty_formatted_json.json', 1), + ('unsorted_pretty_formatted_json.json', 1), + ('non_ascii_pretty_formatted_json.json', 1), + ('pretty_formatted_json.json', 0), + ), +) def test_pretty_format_json(filename, expected_retval): ret = pretty_format_json([get_resource_path(filename)]) assert ret == expected_retval -@pytest.mark.parametrize(('filename', 'expected_retval'), ( - ('not_pretty_formatted_json.json', 1), - ('unsorted_pretty_formatted_json.json', 0), - ('pretty_formatted_json.json', 0), -)) +@pytest.mark.parametrize( + ('filename', 'expected_retval'), ( + ('not_pretty_formatted_json.json', 1), + ('unsorted_pretty_formatted_json.json', 0), + ('non_ascii_pretty_formatted_json.json', 1), + ('pretty_formatted_json.json', 0), + ), +) def test_unsorted_pretty_format_json(filename, expected_retval): ret = pretty_format_json(['--no-sort-keys', get_resource_path(filename)]) assert ret == expected_retval -@pytest.mark.parametrize(('filename', 'expected_retval'), ( - ('not_pretty_formatted_json.json', 1), - ('unsorted_pretty_formatted_json.json', 1), - ('pretty_formatted_json.json', 1), - ('tab_pretty_formatted_json.json', 0), -)) -def test_tab_pretty_format_json(filename, expected_retval): +@pytest.mark.skipif(PY2, reason="Requires Python3") +@pytest.mark.parametrize( + ('filename', 'expected_retval'), ( + ('not_pretty_formatted_json.json', 1), + ('unsorted_pretty_formatted_json.json', 1), + ('non_ascii_pretty_formatted_json.json', 1), + ('pretty_formatted_json.json', 1), + ('tab_pretty_formatted_json.json', 0), + ), +) +def test_tab_pretty_format_json(filename, expected_retval): # pragma: no cover ret = pretty_format_json(['--indent', '\t', get_resource_path(filename)]) assert ret == expected_retval +def test_non_ascii_pretty_format_json(): + ret = pretty_format_json(['--no-ensure-ascii', get_resource_path('non_ascii_pretty_formatted_json.json')]) + assert ret == 0 + + def test_autofix_pretty_format_json(tmpdir): srcfile = tmpdir.join('to_be_json_formatted.json') shutil.copyfile( diff --git a/tests/readme_test.py b/tests/readme_test.py index d479d42..b2d03b3 100644 --- a/tests/readme_test.py +++ b/tests/readme_test.py @@ -7,7 +7,9 @@ import yaml def test_readme_contains_all_hooks(): - readme_contents = io.open('README.md').read() - hooks = yaml.load(io.open('hooks.yaml').read()) + with io.open('README.md', encoding='UTF-8') as f: + readme_contents = f.read() + with io.open('.pre-commit-hooks.yaml', encoding='UTF-8') as f: + hooks = yaml.load(f) for hook in hooks: - assert '`{0}`'.format(hook['id']) in readme_contents + assert '`{}`'.format(hook['id']) in readme_contents diff --git a/tests/requirements_txt_fixer_test.py b/tests/requirements_txt_fixer_test.py index 1c590a5..437cebd 100644 --- a/tests/requirements_txt_fixer_test.py +++ b/tests/requirements_txt_fixer_test.py @@ -1,31 +1,45 @@ import pytest +from pre_commit_hooks.requirements_txt_fixer import FAIL from pre_commit_hooks.requirements_txt_fixer import fix_requirements_txt +from pre_commit_hooks.requirements_txt_fixer import PASS from pre_commit_hooks.requirements_txt_fixer import Requirement -# Input, expected return value, expected output -TESTS = ( - (b'foo\nbar\n', 1, b'bar\nfoo\n'), - (b'bar\nfoo\n', 0, b'bar\nfoo\n'), - (b'#comment1\nfoo\n#comment2\nbar\n', 1, b'#comment2\nbar\n#comment1\nfoo\n'), - (b'#comment1\nbar\n#comment2\nfoo\n', 0, b'#comment1\nbar\n#comment2\nfoo\n'), - (b'#comment\n\nfoo\nbar\n', 1, b'#comment\n\nbar\nfoo\n'), - (b'#comment\n\nbar\nfoo\n', 0, b'#comment\n\nbar\nfoo\n'), - (b'\nfoo\nbar\n', 1, b'bar\n\nfoo\n'), - (b'\nbar\nfoo\n', 0, b'\nbar\nfoo\n'), - (b'pyramid==1\npyramid-foo==2\n', 0, b'pyramid==1\npyramid-foo==2\n'), - (b'ocflib\nDjango\nPyMySQL\n', 1, b'Django\nocflib\nPyMySQL\n'), - (b'-e git+ssh://git_url@tag#egg=ocflib\nDjango\nPyMySQL\n', 1, b'Django\n-e git+ssh://git_url@tag#egg=ocflib\nPyMySQL\n'), + +@pytest.mark.parametrize( + ('input_s', 'expected_retval', 'output'), + ( + (b'', PASS, b''), + (b'\n', PASS, b'\n'), + (b'# intentionally empty\n', PASS, b'# intentionally empty\n'), + (b'foo\n# comment at end\n', PASS, b'foo\n# comment at end\n'), + (b'foo\nbar\n', FAIL, b'bar\nfoo\n'), + (b'bar\nfoo\n', PASS, b'bar\nfoo\n'), + (b'#comment1\nfoo\n#comment2\nbar\n', FAIL, b'#comment2\nbar\n#comment1\nfoo\n'), + (b'#comment1\nbar\n#comment2\nfoo\n', PASS, b'#comment1\nbar\n#comment2\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'\nfoo\nbar\n', FAIL, b'bar\n\nfoo\n'), + (b'\nbar\nfoo\n', PASS, b'\nbar\nfoo\n'), + (b'pyramid==1\npyramid-foo==2\n', PASS, b'pyramid==1\npyramid-foo==2\n'), + (b'ocflib\nDjango\nPyMySQL\n', FAIL, b'Django\nocflib\nPyMySQL\n'), + ( + b'-e git+ssh://git_url@tag#egg=ocflib\nDjango\nPyMySQL\n', + FAIL, + b'Django\n-e git+ssh://git_url@tag#egg=ocflib\nPyMySQL\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'), + ), ) - - -@pytest.mark.parametrize(('input_s', 'expected_retval', 'output'), TESTS) def test_integration(input_s, expected_retval, output, tmpdir): path = tmpdir.join('file.txt') path.write_binary(input_s) - assert fix_requirements_txt([path.strpath]) == expected_retval + output_retval = fix_requirements_txt([path.strpath]) + assert path.read_binary() == output + assert output_retval == expected_retval def test_requirement_object(): diff --git a/tests/sort_simple_yaml_test.py b/tests/sort_simple_yaml_test.py new file mode 100644 index 0000000..176d12f --- /dev/null +++ b/tests/sort_simple_yaml_test.py @@ -0,0 +1,120 @@ +from __future__ import absolute_import +from __future__ import unicode_literals + +import os + +import pytest + +from pre_commit_hooks.sort_simple_yaml import first_key +from pre_commit_hooks.sort_simple_yaml import main +from pre_commit_hooks.sort_simple_yaml import parse_block +from pre_commit_hooks.sort_simple_yaml import parse_blocks +from pre_commit_hooks.sort_simple_yaml import sort + +RETVAL_GOOD = 0 +RETVAL_BAD = 1 +TEST_SORTS = [ + ( + ['c: true', '', 'b: 42', 'a: 19'], + ['b: 42', 'a: 19', '', 'c: true'], + RETVAL_BAD, + ), + + ( + ['# i am', '# a header', '', 'c: true', '', 'b: 42', 'a: 19'], + ['# i am', '# a header', '', 'b: 42', 'a: 19', '', 'c: true'], + RETVAL_BAD, + ), + + ( + ['# i am', '# a header', '', 'already: sorted', '', 'yup: i am'], + ['# i am', '# a header', '', 'already: sorted', '', 'yup: i am'], + RETVAL_GOOD, + ), + + ( + ['# i am', '# a header'], + ['# i am', '# a header'], + RETVAL_GOOD, + ), +] + + +@pytest.mark.parametrize('bad_lines,good_lines,retval', TEST_SORTS) +def test_integration_good_bad_lines(tmpdir, bad_lines, good_lines, retval): + file_path = os.path.join(tmpdir.strpath, 'foo.yaml') + + with open(file_path, 'w') as f: + f.write("\n".join(bad_lines) + "\n") + + assert main([file_path]) == retval + + with open(file_path, 'r') as f: + assert [line.rstrip() for line in f.readlines()] == good_lines + + +def test_parse_header(): + lines = ['# some header', '# is here', '', 'this is not a header'] + assert parse_block(lines, header=True) == ['# some header', '# is here'] + assert lines == ['', 'this is not a header'] + + lines = ['this is not a header'] + assert parse_block(lines, header=True) == [] + assert lines == ['this is not a header'] + + +def test_parse_block(): + # a normal block + lines = ['a: 42', 'b: 17', '', 'c: 19'] + assert parse_block(lines) == ['a: 42', 'b: 17'] + assert lines == ['', 'c: 19'] + + # a block at the end + lines = ['c: 19'] + assert parse_block(lines) == ['c: 19'] + assert lines == [] + + # no block + lines = [] + assert parse_block(lines) == [] + assert lines == [] + + +def test_parse_blocks(): + # normal blocks + lines = ['a: 42', 'b: 17', '', 'c: 19'] + assert parse_blocks(lines) == [['a: 42', 'b: 17'], ['c: 19']] + assert lines == [] + + # a single block + lines = ['a: 42', 'b: 17'] + assert parse_blocks(lines) == [['a: 42', 'b: 17']] + assert lines == [] + + # no blocks + lines = [] + assert parse_blocks(lines) == [] + assert lines == [] + + +def test_first_key(): + # first line + lines = ['a: 42', 'b: 17', '', 'c: 19'] + assert first_key(lines) == 'a: 42' + + # second line + lines = ['# some comment', 'a: 42', 'b: 17', '', 'c: 19'] + assert first_key(lines) == 'a: 42' + + # second line with quotes + lines = ['# some comment', '"a": 42', 'b: 17', '', 'c: 19'] + assert first_key(lines) == 'a": 42' + + # no lines + lines = [] + assert first_key(lines) is None + + +@pytest.mark.parametrize('bad_lines,good_lines,_', TEST_SORTS) +def test_sort(bad_lines, good_lines, _): + assert sort(bad_lines) == good_lines diff --git a/tests/string_fixer_test.py b/tests/string_fixer_test.py index 0429b95..a65213b 100644 --- a/tests/string_fixer_test.py +++ b/tests/string_fixer_test.py @@ -22,16 +22,20 @@ TESTS = ( # Docstring ('""" Foo """', '""" Foo """', 0), ( - textwrap.dedent(""" + textwrap.dedent( + """ x = " \\ foo \\ "\n - """), - textwrap.dedent(""" + """, + ), + textwrap.dedent( + """ x = ' \\ foo \\ '\n - """), + """, + ), 1, ), ('"foo""bar"', "'foo''bar'", 1), diff --git a/tests/tests_should_end_in_test_test.py b/tests/tests_should_end_in_test_test.py index a7aaf52..dc686a5 100644 --- a/tests/tests_should_end_in_test_test.py +++ b/tests/tests_should_end_in_test_test.py @@ -12,7 +12,7 @@ def test_validate_files_one_fails(): def test_validate_files_django_all_pass(): - ret = validate_files(['--django', 'test_foo.py', 'test_bar.py', 'tests/test_baz.py']) + ret = validate_files(['--django', 'tests.py', 'test_foo.py', 'test_bar.py', 'tests/test_baz.py']) assert ret == 0 diff --git a/tests/trailing_whitespace_fixer_test.py b/tests/trailing_whitespace_fixer_test.py index eb2a1d0..7ee9e63 100644 --- a/tests/trailing_whitespace_fixer_test.py +++ b/tests/trailing_whitespace_fixer_test.py @@ -3,7 +3,7 @@ from __future__ import unicode_literals import pytest -from pre_commit_hooks.trailing_whitespace_fixer import fix_trailing_whitespace +from pre_commit_hooks.trailing_whitespace_fixer import main @pytest.mark.parametrize( @@ -16,14 +16,22 @@ from pre_commit_hooks.trailing_whitespace_fixer import fix_trailing_whitespace def test_fixes_trailing_whitespace(input_s, expected, tmpdir): path = tmpdir.join('file.txt') path.write(input_s) - assert fix_trailing_whitespace((path.strpath,)) == 1 + assert main((path.strpath,)) == 1 assert path.read() == expected +def test_ok_no_newline_end_of_file(tmpdir): + filename = tmpdir.join('f') + filename.write_binary(b'foo\nbar') + ret = main((filename.strpath,)) + assert filename.read_binary() == b'foo\nbar' + assert ret == 0 + + def test_ok_with_dos_line_endings(tmpdir): filename = tmpdir.join('f') filename.write_binary(b'foo\r\nbar\r\nbaz\r\n') - ret = fix_trailing_whitespace((filename.strpath,)) + ret = main((filename.strpath,)) assert filename.read_binary() == b'foo\r\nbar\r\nbaz\r\n' assert ret == 0 @@ -31,14 +39,14 @@ def test_ok_with_dos_line_endings(tmpdir): def test_markdown_ok(tmpdir): filename = tmpdir.join('foo.md') filename.write_binary(b'foo \n') - ret = fix_trailing_whitespace((filename.strpath,)) + ret = main((filename.strpath,)) assert filename.read_binary() == b'foo \n' assert ret == 0 # filename, expected input, expected output MD_TESTS_1 = ( - ('foo.md', 'foo \nbar \n ', 'foo \nbar\n\n'), + ('foo.md', 'foo \nbar \n ', 'foo \nbar\n'), ('bar.Markdown', 'bar \nbaz\t\n\t\n', 'bar \nbaz\n\n'), ('.md', 'baz \nquux \t\n\t\n', 'baz\nquux\n\n'), ('txt', 'foo \nbaz \n\t\n', 'foo\nbaz\n\n'), @@ -49,7 +57,7 @@ MD_TESTS_1 = ( def test_fixes_trailing_markdown_whitespace(filename, input_s, output, tmpdir): path = tmpdir.join(filename) path.write(input_s) - ret = fix_trailing_whitespace([path.strpath]) + ret = main([path.strpath]) assert ret == 1 assert path.read() == output @@ -68,16 +76,14 @@ MD_TESTS_2 = ( def test_markdown_linebreak_ext_opt(filename, input_s, output, tmpdir): path = tmpdir.join(filename) path.write(input_s) - ret = fix_trailing_whitespace(( - '--markdown-linebreak-ext=TxT', path.strpath - )) + ret = main(('--markdown-linebreak-ext=TxT', path.strpath)) assert ret == 1 assert path.read() == output # filename, expected input, expected output MD_TESTS_3 = ( - ('foo.baz', 'foo \nbar \n ', 'foo \nbar\n\n'), + ('foo.baz', 'foo \nbar \n ', 'foo \nbar\n'), ('bar', 'bar \nbaz\t\n\t\n', 'bar \nbaz\n\n'), ) @@ -87,9 +93,7 @@ def test_markdown_linebreak_ext_opt_all(filename, input_s, output, tmpdir): path = tmpdir.join(filename) path.write(input_s) # need to make sure filename is not treated as argument to option - ret = fix_trailing_whitespace([ - '--markdown-linebreak-ext=*', path.strpath, - ]) + ret = main(('--markdown-linebreak-ext=*', path.strpath)) assert ret == 1 assert path.read() == output @@ -97,7 +101,7 @@ def test_markdown_linebreak_ext_opt_all(filename, input_s, output, tmpdir): @pytest.mark.parametrize(('arg'), ('--', 'a.b', 'a/b')) def test_markdown_linebreak_ext_badopt(arg): with pytest.raises(SystemExit) as excinfo: - fix_trailing_whitespace(['--markdown-linebreak-ext', arg]) + main(['--markdown-linebreak-ext', arg]) assert excinfo.value.code == 2 @@ -112,19 +116,15 @@ MD_TESTS_4 = ( def test_no_markdown_linebreak_ext_opt(filename, input_s, output, tmpdir): path = tmpdir.join(filename) path.write(input_s) - ret = fix_trailing_whitespace(['--no-markdown-linebreak-ext', path.strpath]) + ret = main(['--no-markdown-linebreak-ext', path.strpath]) assert ret == 1 assert path.read() == output -def test_returns_zero_for_no_changes(): - assert fix_trailing_whitespace([__file__]) == 0 - - def test_preserve_non_utf8_file(tmpdir): non_utf8_bytes_content = b'\xe9 \n\n' path = tmpdir.join('file.txt') path.write_binary(non_utf8_bytes_content) - ret = fix_trailing_whitespace([path.strpath]) + ret = main([path.strpath]) assert ret == 1 assert path.size() == (len(non_utf8_bytes_content) - 1) diff --git a/tox.ini b/tox.ini index 34ec112..0b33d3e 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,6 @@ [tox] -project = pre_commit_hooks # These should match the travis env list -envlist = py27,py34,py35,pypy +envlist = py27,py35,py36,pypy [testenv] deps = -rrequirements-dev.txt @@ -12,18 +11,14 @@ setenv = GIT_AUTHOR_EMAIL = "test@example.com" GIT_COMMITTER_EMAIL = "test@example.com" commands = - # coverage erase - # coverage run -m pytest {posargs:tests} - # coverage report --show-missing --fail-under 100 + coverage erase + coverage run -m pytest {posargs:tests} + coverage report --fail-under 100 pre-commit install -f --install-hooks pre-commit run --all-files -[testenv:venv] -envdir = venv-{[tox]project} -commands = - [flake8] max-line-length=131 [pep8] -ignore=E265,E309,E501 +ignore=E265,E501