Compare commits

..

No commits in common. "main" and "v4.0.0" have entirely different histories.
main ... v4.0.0

84 changed files with 866 additions and 1044 deletions

2
.github/FUNDING.yml vendored Normal file
View file

@ -0,0 +1,2 @@
github: asottile
open_collective: pre-commit

View file

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

5
.gitignore vendored
View file

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

View file

@ -1,41 +1,46 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
rev: v4.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-docstring-first
- id: check-json
- id: check-added-large-files
- id: check-yaml
- id: debug-statements
- id: double-quote-string-fixer
- id: name-tests-test
- id: double-quote-string-fixer
- id: requirements-txt-fixer
- repo: https://github.com/asottile/setup-cfg-fmt
rev: v3.2.0
hooks:
- id: setup-cfg-fmt
- repo: https://github.com/asottile/reorder-python-imports
rev: v3.16.0
hooks:
- id: reorder-python-imports
args: [--py310-plus, --add-import, 'from __future__ import annotations']
- repo: https://github.com/asottile/add-trailing-comma
rev: v4.0.0
hooks:
- id: add-trailing-comma
- repo: https://github.com/asottile/pyupgrade
rev: v3.21.2
hooks:
- id: pyupgrade
args: [--py310-plus]
- repo: https://github.com/hhatto/autopep8
rev: v2.3.2
hooks:
- id: autopep8
- repo: https://github.com/PyCQA/flake8
rev: 7.3.0
rev: 3.9.2
hooks:
- id: flake8
additional_dependencies: [flake8-typing-imports==1.7.0]
- repo: https://github.com/pre-commit/mirrors-autopep8
rev: v1.5.7
hooks:
- id: autopep8
- repo: https://github.com/asottile/reorder_python_imports
rev: v2.5.0
hooks:
- id: reorder-python-imports
args: [--py3-plus]
- repo: https://github.com/asottile/pyupgrade
rev: v2.15.0
hooks:
- id: pyupgrade
args: [--py36-plus]
- repo: https://github.com/asottile/add-trailing-comma
rev: v2.1.0
hooks:
- id: add-trailing-comma
args: [--py36-plus]
- repo: https://github.com/asottile/setup-cfg-fmt
rev: v1.17.0
hooks:
- id: setup-cfg-fmt
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.19.1
rev: v0.812
hooks:
- id: mypy

View file

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

View file

@ -1,177 +1,3 @@
6.0.0 - 2025-08-09
==================
## Fixes
- `check-shebang-scripts-are-executable`: improve error message.
- #1115 PR by @homebysix.
## Migrating
- now requires python >= 3.9.
- #1098 PR by @asottile.
- `file-contents-sorter`: disallow `--unique` and `--ignore-case` at the same
time.
- #1095 PR by @nemacysts.
- #794 issue by @teksturi.
- Removed `check-byte-order-marker` and `fix-encoding-pragma`.
- `check-byte-order-marker`: migrate to `fix-byte-order-marker`.
- `fix-encoding-pragma`: migrate to `pyupgrade`.
- #1034 PR by @mxr.
- #1032 issue by @mxr.
- #522 PR by @jgowdy.
5.0.0 - 2024-10-05
==================
### Features
- `requirements-txt-fixer`: also remove `pkg_resources==...`.
- #850 PR by @ericfrederich.
- #1030 issue by @ericfrederich.
- `check-illegal-windows-names`: new hook!
- #1044 PR by @ericfrederich.
- #589 issue by @ericfrederich.
- #1049 PR by @Jeffrey-Lim.
- `pretty-format-json`: continue processing even if a file has a json error.
- #1039 PR by @amarvin.
- #1038 issue by @amarvin.
### Fixes
- `destroyed-symlinks`: set `stages` to `[pre-commit, pre-push, manual]`
- PR #1085 by @AdrianDC.
### Migrating
- pre-commit-hooks now requires `pre-commit>=3.2.0`.
- use non-deprecated names for `stages`.
- #1093 PR by @asottile.
4.6.0 - 2024-04-06
==================
### Features
- `requirements-txt-fixer`: remove duplicate packages.
- #1014 PR by @vhoulbreque-withings.
- #960 issue @csibe17.
### Migrating
- `fix-encoding-pragma`: deprecated -- will be removed in 5.0.0. use
[pyupgrade](https://github.com/asottile/pyupgrade) or some other tool.
- #1033 PR by @mxr.
- #1032 issue by @mxr.
4.5.0 - 2023-10-07
==================
### Features
- `requirements-txt-fixer`: also sort `constraints.txt` by default.
- #857 PR by @lev-blit.
- #830 issue by @PLPeeters.
- `debug-statements`: add `bpdb` debugger.
- #942 PR by @mwip.
- #941 issue by @mwip.
### Fixes
- `file-contents-sorter`: fix sorting an empty file.
- #944 PR by @RoelAdriaans.
- #935 issue by @paduszyk.
- `double-quote-string-fixer`: don't rewrite inside f-strings in 3.12+.
- #973 PR by @asottile.
- #971 issue by @XuehaiPan.
## Migrating
- now requires python >= 3.8.
- #926 PR by @asottile.
- #927 PR by @asottile.
4.4.0 - 2022-11-23
==================
### Features
- `forbid-submodules`: new hook which outright bans submodules.
- #815 PR by @asottile.
- #707 issue by @ChiefGokhlayeh.
4.3.0 - 2022-06-07
==================
### Features
- `check-executables-have-shebangs`: use `git config core.fileMode` to
determine if it should query `git`.
- #730 PR by @Kurt-von-Laven.
- `name-tests-test`: add `--pytest-test-first` test convention.
- #779 PR by @asottile.
### Fixes
- `check-shebang-scripts-are-executable`: update windows instructions.
- #774 PR by @mdeweerd.
- #770 issue by @mdeweerd.
- `check-toml`: use stdlib `tomllib` when available.
- #771 PR by @DanielNoord.
- #755 issue by @sognetic.
- `check-added-large-files`: don't run on non-file `stages`.
- #778 PR by @asottile.
- #777 issue by @skyj.
4.2.0 - 2022-04-06
==================
### Features
- `name-tests-test`: updated display text.
- #713 PR by @asottile.
- `check-docstring-first`: make output more parsable.
- #748 PR by @asottile.
- `check-merge-conflict`: make output more parsable.
- #748 PR by @asottile.
- `debug-statements`: make output more parsable.
- #748 PR by @asottile.
### Fixes
- `check-merge-conflict`: fix detection of `======` conflict marker on windows.
- #748 PR by @asottile.
### Updating
- Drop python<3.7.
- #719 PR by @asottile.
- Changed default branch from `master` to `main`.
- #744 PR by @asottile.
4.1.0 - 2021-12-22
==================
### Features
- `debug-statements`: add `pdbr` debugger.
- #614 PR by @cansarigol.
- `detect-private-key`: add detection for additional key types.
- #658 PR by @ljmf00.
- `check-executables-have-shebangs`: improve messaging on windows.
- #689 PR by @pujitm.
- #686 issue by @jmerdich.
- `check-added-large-files`: support `--enforce-all` with `git-lfs`.
- #674 PR by @amartani.
- #560 issue by @jeremy-coulon.
### Fixes
- `check-case-conflict`: improve performance.
- #626 PR by @guykisel.
- #625 issue by @guykisel.
- `forbid-new-submodules`: fix false-negatives for `pre-push`.
- #619 PR by @m-khvoinitsky.
- #609 issue by @m-khvoinitsky.
- `check-merge-conflict`: fix execution in git worktrees.
- #662 PR by @errsyn.
- #638 issue by @daschuer.
### Misc.
- Normalize case of hook names and descriptions.
- #671 PR by @dennisroche.
- #673 PR by @revolter.
4.0.1 - 2021-05-16
==================
### Fixes
- `check-shebang-scripts-are-executable` fix entry point.
- #602 issue by @Person-93.
- #603 PR by @scop.
4.0.0 - 2021-05-14
==================

View file

@ -1,5 +1,6 @@
[![build status](https://github.com/pre-commit/pre-commit-hooks/actions/workflows/main.yml/badge.svg)](https://github.com/pre-commit/pre-commit-hooks/actions/workflows/main.yml)
[![pre-commit.ci status](https://results.pre-commit.ci/badge/github/pre-commit/pre-commit-hooks/main.svg)](https://results.pre-commit.ci/latest/github/pre-commit/pre-commit-hooks/main)
[![Build Status](https://asottile.visualstudio.com/asottile/_apis/build/status/pre-commit.pre-commit-hooks?branchName=master)](https://asottile.visualstudio.com/asottile/_build/latest?definitionId=17&branchName=master)
[![Azure DevOps coverage](https://img.shields.io/azure-devops/coverage/asottile/asottile/17/master.svg)](https://dev.azure.com/asottile/asottile/_build/latest?definitionId=17&branchName=master)
[![pre-commit.ci status](https://results.pre-commit.ci/badge/github/pre-commit/pre-commit-hooks/master.svg)](https://results.pre-commit.ci/latest/github/pre-commit/pre-commit-hooks/master)
pre-commit-hooks
================
@ -15,7 +16,7 @@ Add this to your `.pre-commit-config.yaml`
```yaml
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0 # Use the ref you want to point at
rev: v4.0.0 # Use the ref you want to point at
hooks:
- id: trailing-whitespace
# - id: ...
@ -45,18 +46,17 @@ Require literal syntax when initializing empty or zero Python builtin types.
#### `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-illegal-windows-names`
Check for files that cannot be created on Windows.
#### `check-json`
Attempts to load all json files to verify syntax.
#### `check-merge-conflict`
Check for files that contain merge conflict strings.
- `--assume-in-merge` - Allows running the hook when there is no ongoing merge operation
#### `check-shebang-scripts-are-executable`
Checks that scripts with shebangs are executable.
@ -113,28 +113,21 @@ This hook replaces double quoted strings with single quoted strings.
#### `end-of-file-fixer`
Makes sure files end in a newline and only a newline.
#### `file-contents-sorter`
Sort the lines in specified files (defaults to alphabetical).
You must provide the target [`files`](https://pre-commit.com/#config-files) as input.
Note that this hook WILL remove blank lines and does NOT respect any comments.
All newlines will be converted to line feeds (`\n`).
The following arguments are available:
- `--ignore-case` - fold lower case to upper case characters.
- `--unique` - ensure each line is unique.
#### `fix-byte-order-marker`
removes UTF-8 byte order marker
#### `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.
#### `forbid-new-submodules`
Prevent addition of new git submodules.
This is intended as a helper to migrate away from submodules. If you want to
ban them entirely use `forbid-submodules`
#### `forbid-submodules`
forbids any submodules in the repository.
#### `mixed-line-ending`
Replaces or checks mixed line ending.
- `--fix={auto,crlf,lf,no}`
@ -144,15 +137,13 @@ Replaces or checks mixed line ending.
- `no` - Checks if there is any mixed line ending without modifying any file.
#### `name-tests-test`
verifies that test files are named correctly.
- `--pytest` (the default): ensure tests match `.*_test\.py`
- `--pytest-test-first`: ensure tests match `test_.*\.py`
- `--django` / `--unittest`: ensure tests match `test.*\.py`
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, staging, --branch, main]` to set the branch.
Both `main` and `master` are protected by default if no branch argument is set.
- Use `args: [--branch, staging, --branch, master]` to set the branch.
Both `master` and `main` are protected by default if no branch argument is set.
- `-b` / `--branch` may be specified multiple times to protect multiple
branches.
- `-p` / `--pattern` can be used to protect branches that match a supplied regex
@ -176,7 +167,7 @@ the following commandline options:
- `--top-keys comma,separated,keys` - Keys to keep at the top of mappings.
#### `requirements-txt-fixer`
Sorts entries in requirements.txt and constraints.txt and removes incorrect entry for `pkg-resources==0.0.0`
Sorts entries in requirements.txt and removes incorrect entry for `pkg-resources==0.0.0`
#### `sort-simple-yaml`
Sorts simple YAML files which consist only of top-level
@ -203,9 +194,6 @@ Trims trailing whitespace.
### Deprecated / replaced hooks
- `check-byte-order-marker`: instead use fix-byte-order-marker
- `fix-encoding-pragma`: instead use [`pyupgrade`](https://github.com/asottile/pyupgrade)
- `check-docstring-first`: fundamentally flawed, deprecated without replacement.
### As a standalone package

23
azure-pipelines.yml Normal file
View file

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

View file

@ -1,33 +1,24 @@
from __future__ import annotations
import argparse
import json
import math
import os
import subprocess
from collections.abc import Sequence
from typing import Optional
from typing import Sequence
from typing import Set
from pre_commit_hooks.util import added_files
from pre_commit_hooks.util import zsplit
from pre_commit_hooks.util import CalledProcessError
from pre_commit_hooks.util import cmd_output
def filter_lfs_files(filenames: set[str]) -> None: # pragma: no cover (lfs)
"""Remove files tracked by git-lfs from the set."""
if not filenames:
return
def lfs_files() -> Set[str]:
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)
lfs_ret = '{"files":{}}'
check_attr = subprocess.run(
('git', 'check-attr', 'filter', '-z', '--stdin'),
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
encoding='utf-8',
check=True,
input='\0'.join(filenames),
)
stdout = zsplit(check_attr.stdout)
for i in range(0, len(stdout), 3):
filename, filter_tag = stdout[i], stdout[i + 2]
if filter_tag == 'lfs':
filenames.remove(filename)
return set(json.loads(lfs_ret)['files'])
def find_large_added_files(
@ -39,14 +30,12 @@ def find_large_added_files(
# Find all added files that are also in the list of files pre-commit tells
# us about
retv = 0
filenames_filtered = set(filenames)
filter_lfs_files(filenames_filtered)
filenames_filtered = set(filenames) - lfs_files()
if not enforce_all:
filenames_filtered &= added_files()
for filename in filenames_filtered:
kb = math.ceil(os.stat(filename).st_size / 1024)
kb = int(math.ceil(os.stat(filename).st_size / 1024))
if kb > maxkb:
print(f'{filename} ({kb} KB) exceeds {maxkb} KB.')
retv = 1
@ -54,7 +43,7 @@ def find_large_added_files(
return retv
def main(argv: Sequence[str] | None = None) -> int:
def main(argv: Optional[Sequence[str]] = None) -> int:
parser = argparse.ArgumentParser()
parser.add_argument(
'filenames', nargs='*',
@ -66,7 +55,7 @@ def main(argv: Sequence[str] | None = None) -> int:
)
parser.add_argument(
'--maxkb', type=int, default=500,
help='Maximum allowable KB for added files',
help='Maxmimum allowable KB for added files',
)
args = parser.parse_args(argv)
@ -78,4 +67,4 @@ def main(argv: Sequence[str] | None = None) -> int:
if __name__ == '__main__':
raise SystemExit(main())
exit(main())

View file

@ -1,14 +1,13 @@
from __future__ import annotations
import argparse
import ast
import platform
import sys
import traceback
from collections.abc import Sequence
from typing import Optional
from typing import Sequence
def main(argv: Sequence[str] | None = None) -> int:
def main(argv: Optional[Sequence[str]] = None) -> int:
parser = argparse.ArgumentParser()
parser.add_argument('filenames', nargs='*')
args = parser.parse_args(argv)
@ -30,4 +29,4 @@ def main(argv: Sequence[str] | None = None) -> int:
if __name__ == '__main__':
raise SystemExit(main())
exit(main())

View file

@ -1,9 +1,10 @@
from __future__ import annotations
import argparse
import ast
from collections.abc import Sequence
from typing import List
from typing import NamedTuple
from typing import Optional
from typing import Sequence
from typing import Set
BUILTIN_TYPES = {
@ -26,39 +27,38 @@ class Call(NamedTuple):
class Visitor(ast.NodeVisitor):
def __init__(
self,
ignore: set[str],
ignore: Optional[Sequence[str]] = None,
allow_dict_kwargs: bool = True,
) -> None:
self.builtin_type_calls: list[Call] = []
self.builtin_type_calls: List[Call] = []
self.ignore = set(ignore) if ignore else set()
self.allow_dict_kwargs = allow_dict_kwargs
self._disallowed = BUILTIN_TYPES.keys() - ignore
def _check_dict_call(self, node: ast.Call) -> bool:
return self.allow_dict_kwargs and bool(node.keywords)
def visit_Call(self, node: ast.Call) -> None:
if (
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.
isinstance(node.func, ast.Name) and
node.func.id in self._disallowed and
(node.func.id != 'dict' or not self._check_dict_call(node)) and
not node.args
):
self.builtin_type_calls.append(
Call(node.func.id, node.lineno, node.col_offset),
)
self.generic_visit(node)
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(
Call(node.func.id, node.lineno, node.col_offset),
)
def check_file(
filename: str,
*,
ignore: set[str],
ignore: Optional[Sequence[str]] = None,
allow_dict_kwargs: bool = True,
) -> list[Call]:
) -> List[Call]:
with open(filename, 'rb') as f:
tree = ast.parse(f.read(), filename=filename)
visitor = Visitor(ignore=ignore, allow_dict_kwargs=allow_dict_kwargs)
@ -66,11 +66,11 @@ def check_file(
return visitor.builtin_type_calls
def parse_ignore(value: str) -> set[str]:
def parse_ignore(value: str) -> Set[str]:
return set(value.split(','))
def main(argv: Sequence[str] | None = None) -> int:
def main(argv: Optional[Sequence[str]] = None) -> int:
parser = argparse.ArgumentParser()
parser.add_argument('filenames', nargs='*')
parser.add_argument('--ignore', type=parse_ignore, default=set())
@ -103,4 +103,4 @@ def main(argv: Sequence[str] | None = None) -> int:
if __name__ == '__main__':
raise SystemExit(main())
exit(main())

View file

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

View file

@ -1,27 +1,27 @@
from __future__ import annotations
import argparse
from collections.abc import Iterable
from collections.abc import Iterator
from collections.abc import Sequence
import os.path
from typing import Iterable
from typing import Iterator
from typing import Optional
from typing import Sequence
from typing import Set
from pre_commit_hooks.util import added_files
from pre_commit_hooks.util import cmd_output
def lower_set(iterable: Iterable[str]) -> set[str]:
def lower_set(iterable: Iterable[str]) -> Set[str]:
return {x.lower() for x in iterable}
def parents(file: str) -> Iterator[str]:
path_parts = file.split('/')
path_parts.pop()
while path_parts:
yield '/'.join(path_parts)
path_parts.pop()
file = os.path.dirname(file)
while file:
yield file
file = os.path.dirname(file)
def directories_for(files: set[str]) -> set[str]:
def directories_for(files: Set[str]) -> Set[str]:
return {parent for file in files for parent in parents(file)}
@ -56,7 +56,7 @@ def find_conflicting_filenames(filenames: Sequence[str]) -> int:
return retv
def main(argv: Sequence[str] | None = None) -> int:
def main(argv: Optional[Sequence[str]] = None) -> int:
parser = argparse.ArgumentParser()
parser.add_argument(
'filenames', nargs='*',
@ -69,4 +69,4 @@ def main(argv: Sequence[str] | None = None) -> int:
if __name__ == '__main__':
raise SystemExit(main())
exit(main())

View file

@ -1,10 +1,9 @@
from __future__ import annotations
import argparse
import io
import tokenize
from collections.abc import Sequence
from tokenize import tokenize as tokenize_tokenize
from typing import Optional
from typing import Sequence
NON_CODE_TOKENS = frozenset((
tokenize.COMMENT, tokenize.ENDMARKER, tokenize.NEWLINE, tokenize.NL,
@ -28,13 +27,13 @@ def check_docstring_first(src: bytes, filename: str = '<unknown>') -> int:
if tok_type == tokenize.STRING and scol == 0:
if found_docstring_line is not None:
print(
f'{filename}:{sline}: Multiple module docstrings '
f'{filename}:{sline} Multiple module docstrings '
f'(first docstring on line {found_docstring_line}).',
)
return 1
elif found_code_line is not None:
print(
f'{filename}:{sline}: Module docstring appears after code '
f'{filename}:{sline} Module docstring appears after code '
f'(code seen on line {found_code_line}).',
)
return 1
@ -46,7 +45,7 @@ def check_docstring_first(src: bytes, filename: str = '<unknown>') -> int:
return 0
def main(argv: Sequence[str] | None = None) -> int:
def main(argv: Optional[Sequence[str]] = None) -> int:
parser = argparse.ArgumentParser()
parser.add_argument('filenames', nargs='*')
args = parser.parse_args(argv)

View file

@ -1,12 +1,13 @@
"""Check that executable text files have a shebang."""
from __future__ import annotations
import argparse
import shlex
import sys
from collections.abc import Generator
from collections.abc import Sequence
from typing import Generator
from typing import List
from typing import NamedTuple
from typing import Optional
from typing import Sequence
from typing import Set
from pre_commit_hooks.util import cmd_output
from pre_commit_hooks.util import zsplit
@ -14,11 +15,8 @@ from pre_commit_hooks.util import zsplit
EXECUTABLE_VALUES = frozenset(('1', '3', '5', '7'))
def check_executables(paths: list[str]) -> int:
fs_tracks_executable_bit = cmd_output(
'git', 'config', 'core.fileMode', retcode=None,
).strip()
if fs_tracks_executable_bit == 'false': # pragma: win32 cover
def check_executables(paths: List[str]) -> int:
if sys.platform == 'win32': # pragma: win32 cover
return _check_git_filemode(paths)
else: # pragma: win32 no cover
retv = 0
@ -35,7 +33,7 @@ class GitLsFile(NamedTuple):
filename: str
def git_ls_files(paths: Sequence[str]) -> Generator[GitLsFile]:
def git_ls_files(paths: Sequence[str]) -> Generator[GitLsFile, None, None]:
outs = cmd_output('git', 'ls-files', '-z', '--stage', '--', *paths)
for out in zsplit(outs):
metadata, filename = out.split('\t')
@ -44,7 +42,7 @@ def git_ls_files(paths: Sequence[str]) -> Generator[GitLsFile]:
def _check_git_filemode(paths: Sequence[str]) -> int:
seen: set[str] = set()
seen: Set[str] = set()
for ls_file in git_ls_files(paths):
is_executable = any(b in EXECUTABLE_VALUES for b in ls_file.mode[-3:])
if is_executable and not has_shebang(ls_file.filename):
@ -66,14 +64,12 @@ def _message(path: str) -> None:
f'{path}: marked executable but has no (or invalid) shebang!\n'
f" If it isn't supposed to be executable, try: "
f'`chmod -x {shlex.quote(path)}`\n'
f' If on Windows, you may also need to: '
f'`git add --chmod=-x {shlex.quote(path)}`\n'
f' If it is supposed to be executable, double-check its shebang.',
file=sys.stderr,
)
def main(argv: Sequence[str] | None = None) -> int:
def main(argv: Optional[Sequence[str]] = None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('filenames', nargs='*')
args = parser.parse_args(argv)
@ -82,4 +78,4 @@ def main(argv: Sequence[str] | None = None) -> int:
if __name__ == '__main__':
raise SystemExit(main())
exit(main())

View file

@ -1,14 +1,16 @@
from __future__ import annotations
import argparse
import json
from collections.abc import Sequence
from typing import Any
from typing import Dict
from typing import List
from typing import Optional
from typing import Sequence
from typing import Tuple
def raise_duplicate_keys(
ordered_pairs: list[tuple[str, Any]],
) -> dict[str, Any]:
ordered_pairs: List[Tuple[str, Any]],
) -> Dict[str, Any]:
d = {}
for key, val in ordered_pairs:
if key in d:
@ -18,7 +20,7 @@ def raise_duplicate_keys(
return d
def main(argv: Sequence[str] | None = None) -> int:
def main(argv: Optional[Sequence[str]] = None) -> int:
parser = argparse.ArgumentParser()
parser.add_argument('filenames', nargs='*', help='Filenames to check.')
args = parser.parse_args(argv)
@ -35,4 +37,4 @@ def main(argv: Sequence[str] | None = None) -> int:
if __name__ == '__main__':
raise SystemExit(main())
exit(main())

View file

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

View file

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

View file

@ -1,11 +1,10 @@
from __future__ import annotations
import argparse
import os.path
from collections.abc import Sequence
from typing import Optional
from typing import Sequence
def main(argv: Sequence[str] | None = None) -> int:
def main(argv: Optional[Sequence[str]] = None) -> int:
parser = argparse.ArgumentParser(description='Checks for broken symlinks.')
parser.add_argument('filenames', nargs='*', help='Filenames to check')
args = parser.parse_args(argv)
@ -24,4 +23,4 @@ def main(argv: Sequence[str] | None = None) -> int:
if __name__ == '__main__':
raise SystemExit(main())
exit(main())

View file

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

View file

@ -1,10 +1,10 @@
from __future__ import annotations
import argparse
import re
import sys
from collections.abc import Sequence
from re import Pattern
from typing import List
from typing import Optional
from typing import Pattern
from typing import Sequence
def _get_pattern(domain: str) -> Pattern[bytes]:
@ -15,7 +15,7 @@ def _get_pattern(domain: str) -> Pattern[bytes]:
return re.compile(regex.encode())
def _check_filename(filename: str, patterns: list[Pattern[bytes]]) -> int:
def _check_filename(filename: str, patterns: List[Pattern[bytes]]) -> int:
retv = 0
with open(filename, 'rb') as f:
for i, line in enumerate(f, 1):
@ -28,7 +28,7 @@ def _check_filename(filename: str, patterns: list[Pattern[bytes]]) -> int:
return retv
def main(argv: Sequence[str] | None = None) -> int:
def main(argv: Optional[Sequence[str]] = None) -> int:
parser = argparse.ArgumentParser()
parser.add_argument('filenames', nargs='*')
parser.add_argument(
@ -57,4 +57,4 @@ def main(argv: Sequence[str] | None = None) -> int:
if __name__ == '__main__':
raise SystemExit(main())
exit(main())

View file

@ -1,11 +1,10 @@
from __future__ import annotations
import argparse
import xml.sax.handler
from collections.abc import Sequence
from typing import Optional
from typing import Sequence
def main(argv: Sequence[str] | None = None) -> int:
def main(argv: Optional[Sequence[str]] = None) -> int:
parser = argparse.ArgumentParser()
parser.add_argument('filenames', nargs='*', help='XML filenames to check.')
args = parser.parse_args(argv)
@ -23,4 +22,4 @@ def main(argv: Sequence[str] | None = None) -> int:
if __name__ == '__main__':
raise SystemExit(main())
exit(main())

View file

@ -1,17 +1,16 @@
from __future__ import annotations
import argparse
from collections.abc import Generator
from collections.abc import Sequence
from typing import Any
from typing import Generator
from typing import NamedTuple
from typing import Optional
from typing import Sequence
import ruamel.yaml
yaml = ruamel.yaml.YAML(typ='safe')
def _exhaust(gen: Generator[str]) -> None:
def _exhaust(gen: Generator[str, None, None]) -> None:
for _ in gen:
pass
@ -37,7 +36,7 @@ LOAD_FNS = {
}
def main(argv: Sequence[str] | None = None) -> int:
def main(argv: Optional[Sequence[str]] = None) -> int:
parser = argparse.ArgumentParser()
parser.add_argument(
'-m', '--multi', '--allow-multiple-documents', action='store_true',
@ -46,7 +45,7 @@ def main(argv: Sequence[str] | None = None) -> int:
'--unsafe', action='store_true',
help=(
'Instead of loading the files, simply parse them for syntax. '
'A syntax-only check enables extensions and unsafe constructs '
'A syntax-only check enables extensions and unsafe contstructs '
'which would otherwise be forbidden. Using this option removes '
'all guarantees of portability to other yaml implementations. '
'Implies --allow-multiple-documents'
@ -69,4 +68,4 @@ def main(argv: Sequence[str] | None = None) -> int:
if __name__ == '__main__':
raise SystemExit(main())
exit(main())

View file

@ -1,17 +1,15 @@
from __future__ import annotations
import argparse
import ast
import traceback
from collections.abc import Sequence
from typing import List
from typing import NamedTuple
from typing import Optional
from typing import Sequence
DEBUG_STATEMENTS = {
'bpdb',
'ipdb',
'pdb',
'pdbr',
'pudb',
'pydevd_pycharm',
'q',
@ -30,7 +28,7 @@ class Debug(NamedTuple):
class DebugStatementParser(ast.NodeVisitor):
def __init__(self) -> None:
self.breakpoints: list[Debug] = []
self.breakpoints: List[Debug] = []
def visit_Import(self, node: ast.Import) -> None:
for name in node.names:
@ -66,12 +64,12 @@ def check_file(filename: str) -> int:
visitor.visit(ast_obj)
for bp in visitor.breakpoints:
print(f'{filename}:{bp.line}:{bp.col}: {bp.name} {bp.reason}')
print(f'{filename}:{bp.line}:{bp.col} - {bp.name} {bp.reason}')
return int(bool(visitor.breakpoints))
def main(argv: Sequence[str] | None = None) -> int:
def main(argv: Optional[Sequence[str]] = None) -> int:
parser = argparse.ArgumentParser()
parser.add_argument('filenames', nargs='*', help='Filenames to run')
args = parser.parse_args(argv)
@ -83,4 +81,4 @@ def main(argv: Sequence[str] | None = None) -> int:
if __name__ == '__main__':
raise SystemExit(main())
exit(main())

20
pre_commit_hooks/destroyed_symlinks.py Normal file → Executable file
View file

@ -1,9 +1,9 @@
from __future__ import annotations
import argparse
import shlex
import subprocess
from collections.abc import Sequence
from typing import List
from typing import Optional
from typing import Sequence
from pre_commit_hooks.util import cmd_output
from pre_commit_hooks.util import zsplit
@ -13,8 +13,8 @@ PERMS_LINK = '120000'
PERMS_NONEXIST = '000000'
def find_destroyed_symlinks(files: Sequence[str]) -> list[str]:
destroyed_links: list[str] = []
def find_destroyed_symlinks(files: Sequence[str]) -> List[str]:
destroyed_links: List[str] = []
if not files:
return destroyed_links
for line in zsplit(
@ -66,7 +66,7 @@ def find_destroyed_symlinks(files: Sequence[str]) -> list[str]:
return destroyed_links
def main(argv: Sequence[str] | None = None) -> int:
def main(argv: Optional[Sequence[str]] = None) -> int:
parser = argparse.ArgumentParser()
parser.add_argument('filenames', nargs='*', help='Filenames to check.')
args = parser.parse_args(argv)
@ -76,7 +76,11 @@ def main(argv: Sequence[str] | None = None) -> int:
for destroyed_link in destroyed_links:
print(f'- {destroyed_link}')
print('You should unstage affected files:')
print(f'\tgit reset HEAD -- {shlex.join(destroyed_links)}')
print(
'\tgit reset HEAD -- {}'.format(
' '.join(shlex.quote(link) for link in destroyed_links),
),
)
print(
'And retry commit. As a long term solution '
'you may try to explicitly tell git that your '
@ -89,4 +93,4 @@ def main(argv: Sequence[str] | None = None) -> int:
if __name__ == '__main__':
raise SystemExit(main())
exit(main())

View file

@ -1,10 +1,11 @@
from __future__ import annotations
import argparse
import configparser
import os
from collections.abc import Sequence
from typing import List
from typing import NamedTuple
from typing import Optional
from typing import Sequence
from typing import Set
class BadFile(NamedTuple):
@ -12,7 +13,7 @@ class BadFile(NamedTuple):
key: str
def get_aws_cred_files_from_env() -> set[str]:
def get_aws_cred_files_from_env() -> Set[str]:
"""Extract credential file paths from environment variables."""
return {
os.environ[env_var]
@ -24,7 +25,7 @@ def get_aws_cred_files_from_env() -> set[str]:
}
def get_aws_secrets_from_env() -> set[str]:
def get_aws_secrets_from_env() -> Set[str]:
"""Extract AWS secrets from environment variables."""
keys = set()
for env_var in (
@ -35,7 +36,7 @@ def get_aws_secrets_from_env() -> set[str]:
return keys
def get_aws_secrets_from_file(credentials_file: str) -> set[str]:
def get_aws_secrets_from_file(credentials_file: str) -> Set[str]:
"""Extract AWS secrets from configuration files.
Read an ini-style configuration file and return a set with all found AWS
@ -68,8 +69,8 @@ def get_aws_secrets_from_file(credentials_file: str) -> set[str]:
def check_file_for_aws_keys(
filenames: Sequence[str],
keys: set[bytes],
) -> list[BadFile]:
keys: Set[bytes],
) -> List[BadFile]:
"""Check if files contain AWS secrets.
Return a list of all files containing AWS secrets and keys found, with all
@ -89,7 +90,7 @@ def check_file_for_aws_keys(
return bad_files
def main(argv: Sequence[str] | None = None) -> int:
def main(argv: Optional[Sequence[str]] = None) -> int:
parser = argparse.ArgumentParser()
parser.add_argument('filenames', nargs='+', help='Filenames to run')
parser.add_argument(
@ -118,7 +119,7 @@ def main(argv: Sequence[str] | None = None) -> int:
# of files to to gather AWS secrets from.
credential_files |= get_aws_cred_files_from_env()
keys: set[str] = set()
keys: Set[str] = set()
for credential_file in credential_files:
keys |= get_aws_secrets_from_file(credential_file)
@ -148,4 +149,4 @@ def main(argv: Sequence[str] | None = None) -> int:
if __name__ == '__main__':
raise SystemExit(main())
exit(main())

View file

@ -1,7 +1,6 @@
from __future__ import annotations
import argparse
from collections.abc import Sequence
from typing import Optional
from typing import Sequence
BLACKLIST = [
b'BEGIN RSA PRIVATE KEY',
@ -12,12 +11,10 @@ BLACKLIST = [
b'PuTTY-User-Key-File-2',
b'BEGIN SSH2 ENCRYPTED PRIVATE KEY',
b'BEGIN PGP PRIVATE KEY BLOCK',
b'BEGIN ENCRYPTED PRIVATE KEY',
b'BEGIN OpenVPN Static key V1',
]
def main(argv: Sequence[str] | None = None) -> int:
def main(argv: Optional[Sequence[str]] = None) -> int:
parser = argparse.ArgumentParser()
parser.add_argument('filenames', nargs='*', help='Filenames to check')
args = parser.parse_args(argv)
@ -39,4 +36,4 @@ def main(argv: Sequence[str] | None = None) -> int:
if __name__ == '__main__':
raise SystemExit(main())
exit(main())

View file

@ -1,9 +1,8 @@
from __future__ import annotations
import argparse
import os
from collections.abc import Sequence
from typing import IO
from typing import Optional
from typing import Sequence
def fix_file(file_obj: IO[bytes]) -> int:
@ -49,7 +48,7 @@ def fix_file(file_obj: IO[bytes]) -> int:
return 0
def main(argv: Sequence[str] | None = None) -> int:
def main(argv: Optional[Sequence[str]] = None) -> int:
parser = argparse.ArgumentParser()
parser.add_argument('filenames', nargs='*', help='Filenames to fix')
args = parser.parse_args(argv)
@ -68,4 +67,4 @@ def main(argv: Sequence[str] | None = None) -> int:
if __name__ == '__main__':
raise SystemExit(main())
exit(main())

View file

@ -9,14 +9,13 @@ 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 annotations
import argparse
from collections.abc import Callable
from collections.abc import Iterable
from collections.abc import Sequence
from typing import Any
from typing import Callable
from typing import IO
from typing import Iterable
from typing import Optional
from typing import Sequence
PASS = 0
FAIL = 1
@ -24,7 +23,7 @@ FAIL = 1
def sort_file_contents(
f: IO[bytes],
key: Callable[[bytes], Any] | None,
key: Optional[Callable[[bytes], Any]],
*,
unique: bool = False,
) -> int:
@ -37,10 +36,7 @@ def sort_file_contents(
after = sorted(lines, key=key)
before_string = b''.join(before)
after_string = b'\n'.join(after)
if after_string:
after_string += b'\n'
after_string = b'\n'.join(after) + b'\n'
if before_string == after_string:
return PASS
@ -51,24 +47,21 @@ def sort_file_contents(
return FAIL
def main(argv: Sequence[str] | None = None) -> int:
def main(argv: Optional[Sequence[str]] = None) -> int:
parser = argparse.ArgumentParser()
parser.add_argument('filenames', nargs='+', help='Files to sort')
mutex = parser.add_mutually_exclusive_group(required=False)
mutex.add_argument(
parser.add_argument(
'--ignore-case',
action='store_const',
const=bytes.lower,
default=None,
help='fold lower case to upper case characters',
)
mutex.add_argument(
parser.add_argument(
'--unique',
action='store_true',
help='ensure each line is unique',
)
args = parser.parse_args(argv)
retv = PASS
@ -88,4 +81,4 @@ def main(argv: Sequence[str] | None = None) -> int:
if __name__ == '__main__':
raise SystemExit(main())
exit(main())

View file

@ -1,10 +1,9 @@
from __future__ import annotations
import argparse
from collections.abc import Sequence
from typing import Optional
from typing import Sequence
def main(argv: Sequence[str] | None = None) -> int:
def main(argv: Optional[Sequence[str]] = None) -> int:
parser = argparse.ArgumentParser()
parser.add_argument('filenames', nargs='*', help='Filenames to check')
args = parser.parse_args(argv)
@ -28,4 +27,4 @@ def main(argv: Sequence[str] | None = None) -> int:
if __name__ == '__main__':
raise SystemExit(main())
exit(main())

View file

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

View file

@ -1,30 +1,14 @@
from __future__ import annotations
import argparse
import os
from collections.abc import Sequence
from typing import Optional
from typing import Sequence
from pre_commit_hooks.util import cmd_output
def main(argv: Sequence[str] | None = None) -> int:
parser = argparse.ArgumentParser()
parser.add_argument('filenames', nargs='*')
args = parser.parse_args(argv)
if (
'PRE_COMMIT_FROM_REF' in os.environ and
'PRE_COMMIT_TO_REF' in os.environ
):
diff_arg = '...'.join((
os.environ['PRE_COMMIT_FROM_REF'],
os.environ['PRE_COMMIT_TO_REF'],
))
else:
diff_arg = '--staged'
def main(argv: Optional[Sequence[str]] = None) -> int:
# `argv` is ignored, pre-commit will send us a list of files that we
# don't care about
added_diff = cmd_output(
'git', 'diff', '--diff-filter=A', '--raw', diff_arg, '--',
*args.filenames,
'git', 'diff', '--staged', '--diff-filter=A', '--raw',
)
retv = 0
for line in added_diff.splitlines():
@ -45,4 +29,4 @@ def main(argv: Sequence[str] | None = None) -> int:
if __name__ == '__main__':
raise SystemExit(main())
exit(main())

View file

@ -1,8 +1,8 @@
from __future__ import annotations
import argparse
import collections
from collections.abc import Sequence
from typing import Dict
from typing import Optional
from typing import Sequence
CRLF = b'\r\n'
@ -25,7 +25,7 @@ def fix_filename(filename: str, fix: str) -> int:
with open(filename, 'rb') as f:
contents = f.read()
counts: dict[bytes, int] = collections.defaultdict(int)
counts: Dict[bytes, int] = collections.defaultdict(int)
for line in contents.splitlines(True):
for ending in ALL_ENDINGS:
@ -62,7 +62,7 @@ def fix_filename(filename: str, fix: str) -> int:
return other_endings
def main(argv: Sequence[str] | None = None) -> int:
def main(argv: Optional[Sequence[str]] = None) -> int:
parser = argparse.ArgumentParser()
parser.add_argument(
'-f', '--fix',
@ -85,4 +85,4 @@ def main(argv: Sequence[str] | None = None) -> int:
if __name__ == '__main__':
raise SystemExit(main())
exit(main())

View file

@ -1,9 +1,8 @@
from __future__ import annotations
import argparse
import re
from collections.abc import Sequence
from typing import AbstractSet
from typing import Optional
from typing import Sequence
from pre_commit_hooks.util import CalledProcessError
from pre_commit_hooks.util import cmd_output
@ -24,7 +23,7 @@ def is_on_branch(
)
def main(argv: Sequence[str] | None = None) -> int:
def main(argv: Optional[Sequence[str]] = None) -> int:
parser = argparse.ArgumentParser()
parser.add_argument(
'-b', '--branch', action='append',
@ -45,4 +44,4 @@ def main(argv: Sequence[str] | None = None) -> int:
if __name__ == '__main__':
raise SystemExit(main())
exit(main())

View file

@ -1,11 +1,13 @@
from __future__ import annotations
import argparse
import json
import sys
from collections.abc import Mapping
from collections.abc import Sequence
from difflib import unified_diff
from typing import List
from typing import Mapping
from typing import Optional
from typing import Sequence
from typing import Tuple
from typing import Union
def _get_pretty_format(
@ -15,7 +17,7 @@ def _get_pretty_format(
sort_keys: bool = True,
top_keys: Sequence[str] = (),
) -> str:
def pairs_first(pairs: Sequence[tuple[str, str]]) -> Mapping[str, str]:
def pairs_first(pairs: Sequence[Tuple[str, str]]) -> Mapping[str, str]:
before = [pair for pair in pairs if pair[0] in top_keys]
before = sorted(before, key=lambda x: top_keys.index(x[0]))
after = [pair for pair in pairs if pair[0] not in top_keys]
@ -36,7 +38,7 @@ def _autofix(filename: str, new_contents: str) -> None:
f.write(new_contents)
def parse_num_to_int(s: str) -> int | str:
def parse_num_to_int(s: str) -> Union[int, str]:
"""Convert string numbers to int, leaving strings as is."""
try:
return int(s)
@ -44,7 +46,7 @@ def parse_num_to_int(s: str) -> int | str:
return s
def parse_topkeys(s: str) -> list[str]:
def parse_topkeys(s: str) -> List[str]:
return s.split(',')
@ -55,7 +57,7 @@ def get_diff(source: str, target: str, file: str) -> str:
return ''.join(diff)
def main(argv: Sequence[str] | None = None) -> int:
def main(argv: Optional[Sequence[str]] = None) -> int:
parser = argparse.ArgumentParser()
parser.add_argument(
'--autofix',
@ -115,23 +117,19 @@ def main(argv: Sequence[str] | None = None) -> int:
f'Input File {json_file} is not a valid JSON, consider using '
f'check-json',
)
status = 1
else:
if contents != pretty_contents:
if args.autofix:
_autofix(json_file, pretty_contents)
else:
diff_output = get_diff(
contents,
pretty_contents,
json_file,
)
sys.stdout.buffer.write(diff_output.encode())
return 1
status = 1
if contents != pretty_contents:
if args.autofix:
_autofix(json_file, pretty_contents)
else:
diff_output = get_diff(contents, pretty_contents, json_file)
sys.stdout.buffer.write(diff_output.encode())
status = 1
return status
if __name__ == '__main__':
raise SystemExit(main())
exit(main())

View file

@ -1,10 +1,9 @@
from __future__ import annotations
import sys
from collections.abc import Sequence
from typing import Optional
from typing import Sequence
def main(argv: Sequence[str] | None = None) -> int:
def main(argv: Optional[Sequence[str]] = None) -> int:
argv = argv if argv is not None else sys.argv[1:]
hookid, new_hookid, url = argv[:3]
raise SystemExit(
@ -13,4 +12,4 @@ def main(argv: Sequence[str] | None = None) -> int:
if __name__ == '__main__':
raise SystemExit(main())
exit(main())

View file

@ -1,9 +1,9 @@
from __future__ import annotations
import argparse
import re
from collections.abc import Sequence
from typing import IO
from typing import List
from typing import Optional
from typing import Sequence
PASS = 0
@ -15,8 +15,8 @@ class Requirement:
UNTIL_SEP = re.compile(rb'[^;\s]+')
def __init__(self) -> None:
self.value: bytes | None = None
self.comments: list[bytes] = []
self.value: Optional[bytes] = None
self.comments: List[bytes] = []
@property
def name(self) -> bytes:
@ -36,7 +36,7 @@ class Requirement:
return name[:m.start()]
def __lt__(self, requirement: Requirement) -> bool:
def __lt__(self, requirement: 'Requirement') -> bool:
# \n means top of file comment, so always return True,
# otherwise just do a string comparison with value.
assert self.value is not None, self.value
@ -45,11 +45,6 @@ class Requirement:
elif requirement.value == b'\n':
return False
else:
# if 2 requirements have the same name, the one with comments
# needs to go first (so that when removing duplicates, the one
# with comments is kept)
if self.name == requirement.name:
return bool(self.comments) > bool(requirement.comments)
return self.name < requirement.name
def is_complete(self) -> bool:
@ -66,9 +61,9 @@ class Requirement:
def fix_requirements(f: IO[bytes]) -> int:
requirements: list[Requirement] = []
requirements: List[Requirement] = []
before = list(f)
after: list[bytes] = []
after: List[bytes] = []
before_string = b''.join(before)
@ -115,20 +110,13 @@ def fix_requirements(f: IO[bytes]) -> int:
# which is automatically added by broken pip package under Debian
requirements = [
req for req in requirements
if req.value not in [
b'pkg-resources==0.0.0\n',
b'pkg_resources==0.0.0\n',
]
if req.value != b'pkg-resources==0.0.0\n'
]
# sort the requirements and remove duplicates
prev = None
for requirement in sorted(requirements):
after.extend(requirement.comments)
assert requirement.value, requirement.value
if prev is None or requirement.value != prev.value:
after.append(requirement.value)
prev = requirement
after.append(requirement.value)
after.extend(rest)
after_string = b''.join(after)
@ -142,7 +130,7 @@ def fix_requirements(f: IO[bytes]) -> int:
return FAIL
def main(argv: Sequence[str] | None = None) -> int:
def main(argv: Optional[Sequence[str]] = None) -> int:
parser = argparse.ArgumentParser()
parser.add_argument('filenames', nargs='*', help='Filenames to fix')
args = parser.parse_args(argv)
@ -162,4 +150,4 @@ def main(argv: Sequence[str] | None = None) -> int:
if __name__ == '__main__':
raise SystemExit(main())
exit(main())

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

@ -1,3 +1,4 @@
#!/usr/bin/env python
"""Sort a simple YAML file, keeping blocks of comments and definitions
together.
@ -17,16 +18,16 @@ We assume a strict subset of YAML that looks like:
In other words, we don't sort deeper than the top layer, and might corrupt
complicated YAML files.
"""
from __future__ import annotations
import argparse
from collections.abc import Sequence
from typing import List
from typing import Optional
from typing import Sequence
QUOTES = ["'", '"']
def sort(lines: list[str]) -> list[str]:
def sort(lines: List[str]) -> List[str]:
"""Sort a YAML file in alphabetical order, keeping blocks together.
:param lines: array of strings (without newlines)
@ -44,7 +45,7 @@ def sort(lines: list[str]) -> list[str]:
return new_lines
def parse_block(lines: list[str], header: bool = False) -> list[str]:
def parse_block(lines: List[str], header: bool = False) -> List[str]:
"""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
@ -60,7 +61,7 @@ def parse_block(lines: list[str], header: bool = False) -> list[str]:
return block_lines
def parse_blocks(lines: list[str]) -> list[list[str]]:
def parse_blocks(lines: List[str]) -> List[List[str]]:
"""Parse and return all possible blocks, popping off the start of `lines`.
:param lines: list of lines
@ -77,7 +78,7 @@ def parse_blocks(lines: list[str]) -> list[list[str]]:
return blocks
def first_key(lines: list[str]) -> str:
def first_key(lines: List[str]) -> str:
"""Returns a string representing the sort key of a block.
The sort key is the first YAML key we encounter, ignoring comments, and
@ -99,7 +100,7 @@ def first_key(lines: list[str]) -> str:
return '' # not actually reached in reality
def main(argv: Sequence[str] | None = None) -> int:
def main(argv: Optional[Sequence[str]] = None) -> int:
parser = argparse.ArgumentParser()
parser.add_argument('filenames', nargs='*', help='Filenames to fix')
args = parser.parse_args(argv)
@ -122,4 +123,4 @@ def main(argv: Sequence[str] | None = None) -> int:
if __name__ == '__main__':
raise SystemExit(main())
exit(main())

View file

@ -1,17 +1,10 @@
from __future__ import annotations
import argparse
import io
import re
import sys
import tokenize
from collections.abc import Sequence
if sys.version_info >= (3, 12): # pragma: >=3.12 cover
FSTRING_START = tokenize.FSTRING_START
FSTRING_END = tokenize.FSTRING_END
else: # pragma: <3.12 cover
FSTRING_START = FSTRING_END = -1
from typing import List
from typing import Optional
from typing import Sequence
START_QUOTE_RE = re.compile('^[a-zA-Z]*"')
@ -31,7 +24,7 @@ def handle_match(token_text: str) -> str:
return token_text
def get_line_offsets_by_line_no(src: str) -> list[int]:
def get_line_offsets_by_line_no(src: str) -> List[int]:
# Padded so we can index with line number
offsets = [-1, 0]
for line in src.splitlines(True):
@ -47,17 +40,11 @@ def fix_strings(filename: str) -> int:
# Basically a mutable string
splitcontents = list(contents)
fstring_depth = 0
# Iterate in reverse so the offsets are always correct
tokens_l = list(tokenize.generate_tokens(io.StringIO(contents).readline))
tokens = reversed(tokens_l)
for token_type, token_text, (srow, scol), (erow, ecol), _ in tokens:
if token_type == FSTRING_START: # pragma: >=3.12 cover
fstring_depth += 1
elif token_type == FSTRING_END: # pragma: >=3.12 cover
fstring_depth -= 1
elif fstring_depth == 0 and token_type == tokenize.STRING:
if token_type == tokenize.STRING:
new_text = handle_match(token_text)
splitcontents[
line_offsets[srow] + scol:
@ -73,7 +60,7 @@ def fix_strings(filename: str) -> int:
return 0
def main(argv: Sequence[str] | None = None) -> int:
def main(argv: Optional[Sequence[str]] = None) -> int:
parser = argparse.ArgumentParser()
parser.add_argument('filenames', nargs='*', help='Filenames to fix')
args = parser.parse_args(argv)
@ -90,4 +77,4 @@ def main(argv: Sequence[str] | None = None) -> int:
if __name__ == '__main__':
raise SystemExit(main())
exit(main())

View file

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

View file

@ -1,14 +1,13 @@
from __future__ import annotations
import argparse
import os
from collections.abc import Sequence
from typing import Optional
from typing import Sequence
def _fix_file(
filename: str,
is_markdown: bool,
chars: bytes | None,
chars: Optional[bytes],
) -> bool:
with open(filename, mode='rb') as file_processed:
lines = file_processed.readlines()
@ -25,7 +24,7 @@ def _fix_file(
def _process_line(
line: bytes,
is_markdown: bool,
chars: bytes | None,
chars: Optional[bytes],
) -> bytes:
if line[-2:] == b'\r\n':
eol = b'\r\n'
@ -41,7 +40,7 @@ def _process_line(
return line.rstrip(chars) + eol
def main(argv: Sequence[str] | None = None) -> int:
def main(argv: Optional[Sequence[str]] = None) -> int:
parser = argparse.ArgumentParser()
parser.add_argument(
'--no-markdown-linebreak-ext',
@ -100,4 +99,4 @@ def main(argv: Sequence[str] | None = None) -> int:
if __name__ == '__main__':
raise SystemExit(main())
exit(main())

View file

@ -1,19 +1,20 @@
from __future__ import annotations
import subprocess
from typing import Any
from typing import List
from typing import Optional
from typing import Set
class CalledProcessError(RuntimeError):
pass
def added_files() -> set[str]:
def added_files() -> Set[str]:
cmd = ('git', 'diff', '--staged', '--name-only', '--diff-filter=A')
return set(cmd_output(*cmd).splitlines())
def cmd_output(*cmd: str, retcode: int | None = 0, **kwargs: Any) -> str:
def cmd_output(*cmd: str, retcode: Optional[int] = 0, **kwargs: Any) -> str:
kwargs.setdefault('stdout', subprocess.PIPE)
kwargs.setdefault('stderr', subprocess.PIPE)
proc = subprocess.Popen(cmd, **kwargs)
@ -24,7 +25,7 @@ def cmd_output(*cmd: str, retcode: int | None = 0, **kwargs: Any) -> str:
return stdout
def zsplit(s: str) -> list[str]:
def zsplit(s: str) -> List[str]:
s = s.strip('\0')
if s:
return s.split('\0')

View file

@ -1,6 +1,6 @@
[metadata]
name = pre_commit_hooks
version = 6.0.0
version = 4.0.0
description = Some out-of-the-box hooks for pre-commit.
long_description = file: README.md
long_description_content_type = text/markdown
@ -8,10 +8,15 @@ url = https://github.com/pre-commit/pre-commit-hooks
author = Anthony Sottile
author_email = asottile@umich.edu
license = MIT
license_files = LICENSE
license_file = LICENSE
classifiers =
License :: OSI Approved :: MIT License
Programming Language :: Python :: 3
Programming Language :: Python :: 3 :: Only
Programming Language :: Python :: 3.6
Programming Language :: Python :: 3.7
Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9
Programming Language :: Python :: Implementation :: CPython
Programming Language :: Python :: Implementation :: PyPy
@ -19,8 +24,8 @@ classifiers =
packages = find:
install_requires =
ruamel.yaml>=0.15
tomli>=1.1.0;python_version<"3.11"
python_requires = >=3.10
toml
python_requires = >=3.6.1
[options.packages.find]
exclude =
@ -32,12 +37,13 @@ console_scripts =
check-added-large-files = pre_commit_hooks.check_added_large_files:main
check-ast = pre_commit_hooks.check_ast:main
check-builtin-literals = pre_commit_hooks.check_builtin_literals:main
check-byte-order-marker = pre_commit_hooks.check_byte_order_marker:main
check-case-conflict = pre_commit_hooks.check_case_conflict:main
check-docstring-first = pre_commit_hooks.check_docstring_first:main
check-executables-have-shebangs = pre_commit_hooks.check_executables_have_shebangs:main
check-json = pre_commit_hooks.check_json:main
check-merge-conflict = pre_commit_hooks.check_merge_conflict:main
check-shebang-scripts-are-executable = pre_commit_hooks.check_shebang_scripts_are_executable:main
check-shebang-scripts-are-executable = pre_commit_hooks.check_executables_have_shebangs:main_reverse
check-symlinks = pre_commit_hooks.check_symlinks:main
check-toml = pre_commit_hooks.check_toml:main
check-vcs-permalinks = pre_commit_hooks.check_vcs_permalinks:main
@ -51,6 +57,7 @@ console_scripts =
end-of-file-fixer = pre_commit_hooks.end_of_file_fixer:main
file-contents-sorter = pre_commit_hooks.file_contents_sorter:main
fix-byte-order-marker = pre_commit_hooks.fix_byte_order_marker:main
fix-encoding-pragma = pre_commit_hooks.fix_encoding_pragma:main
forbid-new-submodules = pre_commit_hooks.forbid_new_submodules:main
mixed-line-ending = pre_commit_hooks.mixed_line_ending:main
name-tests-test = pre_commit_hooks.tests_should_end_in_test:main
@ -72,8 +79,7 @@ check_untyped_defs = true
disallow_any_generics = true
disallow_incomplete_defs = true
disallow_untyped_defs = true
warn_redundant_casts = true
warn_unused_ignores = true
no_implicit_optional = true
[mypy-testing.*]
disallow_untyped_defs = false

View file

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

View file

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

View file

@ -1,13 +1,10 @@
from __future__ import annotations
import shutil
import distutils.spawn
import pytest
from pre_commit_hooks.check_added_large_files import find_large_added_files
from pre_commit_hooks.check_added_large_files import main
from pre_commit_hooks.util import cmd_output
from testing.util import git_commit
def test_nothing_added(temp_git_dir):
@ -78,7 +75,7 @@ def test_integration(temp_git_dir):
def has_gitlfs():
return shutil.which('git-lfs') is not None
return distutils.spawn.find_executable('git-lfs') is not None
xfailif_no_gitlfs = pytest.mark.xfail(
@ -87,9 +84,10 @@ xfailif_no_gitlfs = pytest.mark.xfail(
@xfailif_no_gitlfs
def test_allows_gitlfs(temp_git_dir): # pragma: no cover
def test_allows_gitlfs(temp_git_dir, monkeypatch): # pragma: no cover
with temp_git_dir.as_cwd():
cmd_output('git', 'lfs', 'install', '--local')
monkeypatch.setenv('HOME', str(temp_git_dir))
cmd_output('git', 'lfs', 'install')
temp_git_dir.join('f.py').write('a' * 10000)
cmd_output('git', 'lfs', 'track', 'f.py')
cmd_output('git', 'add', '--', '.')
@ -98,37 +96,27 @@ def test_allows_gitlfs(temp_git_dir): # pragma: no cover
@xfailif_no_gitlfs
def test_moves_with_gitlfs(temp_git_dir): # pragma: no cover
def test_moves_with_gitlfs(temp_git_dir, monkeypatch): # pragma: no cover
with temp_git_dir.as_cwd():
cmd_output('git', 'lfs', 'install', '--local')
monkeypatch.setenv('HOME', str(temp_git_dir))
cmd_output('git', 'lfs', 'install')
cmd_output('git', 'lfs', 'track', 'a.bin', 'b.bin')
# First add the file we're going to move
temp_git_dir.join('a.bin').write('a' * 10000)
cmd_output('git', 'add', '--', '.')
git_commit('-am', 'foo')
cmd_output('git', 'commit', '--no-gpg-sign', '-am', 'foo')
# Now move it and make sure the hook still succeeds
cmd_output('git', 'mv', 'a.bin', 'b.bin')
assert main(('--maxkb', '9', 'b.bin')) == 0
@xfailif_no_gitlfs
def test_enforce_allows_gitlfs(temp_git_dir): # pragma: no cover
def test_enforce_allows_gitlfs(temp_git_dir, monkeypatch): # pragma: no cover
with temp_git_dir.as_cwd():
cmd_output('git', 'lfs', 'install', '--local')
monkeypatch.setenv('HOME', str(temp_git_dir))
cmd_output('git', 'lfs', 'install')
temp_git_dir.join('f.py').write('a' * 10000)
cmd_output('git', 'lfs', 'track', 'f.py')
cmd_output('git', 'add', '--', '.')
# With --enforce-all large files on git lfs should succeed
assert main(('--enforce-all', '--maxkb', '9', 'f.py')) == 0
@xfailif_no_gitlfs
def test_enforce_allows_gitlfs_after_commit(temp_git_dir): # pragma: no cover
with temp_git_dir.as_cwd():
cmd_output('git', 'lfs', 'install', '--local')
temp_git_dir.join('f.py').write('a' * 10000)
cmd_output('git', 'lfs', 'track', 'f.py')
cmd_output('git', 'add', '--', '.')
git_commit('-am', 'foo')
# With --enforce-all large files on git lfs should succeed
assert main(('--enforce-all', '--maxkb', '9', 'f.py')) == 0

View file

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

View file

@ -1,5 +1,3 @@
from __future__ import annotations
import ast
import pytest
@ -38,6 +36,11 @@ t1 = ()
'''
@pytest.fixture
def visitor():
return Visitor()
@pytest.mark.parametrize(
('expression', 'calls'),
[
@ -80,8 +83,7 @@ t1 = ()
('builtins.tuple()', []),
],
)
def test_non_dict_exprs(expression, calls):
visitor = Visitor(ignore=set())
def test_non_dict_exprs(visitor, expression, calls):
visitor.visit(ast.parse(expression))
assert visitor.builtin_type_calls == calls
@ -98,8 +100,7 @@ def test_non_dict_exprs(expression, calls):
('builtins.dict()', []),
],
)
def test_dict_allow_kwargs_exprs(expression, calls):
visitor = Visitor(ignore=set())
def test_dict_allow_kwargs_exprs(visitor, expression, calls):
visitor.visit(ast.parse(expression))
assert visitor.builtin_type_calls == calls
@ -111,18 +112,17 @@ def test_dict_allow_kwargs_exprs(expression, calls):
('dict(a=1, b=2, c=3)', [Call('dict', 1, 0)]),
("dict(**{'a': 1, 'b': 2, 'c': 3})", [Call('dict', 1, 0)]),
('builtins.dict()', []),
pytest.param('f(dict())', [Call('dict', 1, 2)], id='nested'),
],
)
def test_dict_no_allow_kwargs_exprs(expression, calls):
visitor = Visitor(ignore=set(), allow_dict_kwargs=False)
visitor = Visitor(allow_dict_kwargs=False)
visitor.visit(ast.parse(expression))
assert visitor.builtin_type_calls == calls
def test_ignore_constructors():
visitor = Visitor(
ignore={'complex', 'dict', 'float', 'int', 'list', 'str', 'tuple'},
ignore=('complex', 'dict', 'float', 'int', 'list', 'str', 'tuple'),
)
visitor.visit(ast.parse(BUILTIN_CONSTRUCTORS))
assert visitor.builtin_type_calls == []

View file

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

View file

@ -1,5 +1,3 @@
from __future__ import annotations
import sys
import pytest
@ -8,7 +6,6 @@ from pre_commit_hooks.check_case_conflict import find_conflicting_filenames
from pre_commit_hooks.check_case_conflict import main
from pre_commit_hooks.check_case_conflict import parents
from pre_commit_hooks.util import cmd_output
from testing.util import git_commit
skip_win32 = pytest.mark.skipif(
sys.platform == 'win32',
@ -88,7 +85,7 @@ def test_file_conflicts_with_committed_file(temp_git_dir):
with temp_git_dir.as_cwd():
temp_git_dir.join('f.py').write("print('hello world')")
cmd_output('git', 'add', 'f.py')
git_commit('-m', 'Add f.py')
cmd_output('git', 'commit', '--no-gpg-sign', '-n', '-m', 'Add f.py')
temp_git_dir.join('F.py').write("print('hello world')")
cmd_output('git', 'add', 'F.py')
@ -101,7 +98,7 @@ def test_file_conflicts_with_committed_dir(temp_git_dir):
with temp_git_dir.as_cwd():
temp_git_dir.mkdir('dir').join('x').write('foo')
cmd_output('git', 'add', '-A')
git_commit('-m', 'Add f.py')
cmd_output('git', 'commit', '--no-gpg-sign', '-n', '-m', 'Add f.py')
temp_git_dir.join('DIR').write('foo')
cmd_output('git', 'add', '-A')

View file

@ -1,5 +1,3 @@
from __future__ import annotations
import pytest
from pre_commit_hooks.check_docstring_first import check_docstring_first
@ -17,7 +15,7 @@ TESTS = (
b'from __future__ import unicode_literals\n'
b'"foo"\n',
1,
'{filename}:2: Module docstring appears after code '
'{filename}:2 Module docstring appears after code '
'(code seen on line 1).\n',
),
# Test double docstring
@ -26,7 +24,7 @@ TESTS = (
b'from __future__ import absolute_import\n'
b'"fake docstring"\n',
1,
'{filename}:3: Multiple module docstrings '
'{filename}:3 Multiple module docstrings '
'(first docstring on line 1).\n',
),
# Test multiple lines of code above
@ -35,7 +33,7 @@ TESTS = (
b'import sys\n'
b'"docstring"\n',
1,
'{filename}:3: Module docstring appears after code '
'{filename}:3 Module docstring appears after code '
'(code seen on line 1).\n',
),
# String literals in expressions are ok.

View file

@ -1,5 +1,3 @@
from __future__ import annotations
import os
import sys

View file

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

View file

@ -1,5 +1,3 @@
from __future__ import annotations
import pytest
from pre_commit_hooks.check_json import main
@ -11,7 +9,7 @@ from testing.util import get_resource_path
('bad_json.notjson', 1),
('bad_json_latin1.nonjson', 1),
('ok_json.json', 0),
('duplicate_key_json.notjson', 1),
('duplicate_key_json.json', 1),
),
)
def test_main(capsys, filename, expected_retval):

View file

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

View file

@ -1,5 +1,3 @@
from __future__ import annotations
import os
import pytest

View file

@ -1,5 +1,3 @@
from __future__ import annotations
import os
import pytest

View file

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

View file

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

View file

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

View file

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

View file

@ -1,5 +1,3 @@
from __future__ import annotations
import pytest
from pre_commit_hooks.util import cmd_output

View file

@ -1,5 +1,3 @@
from __future__ import annotations
import ast
from pre_commit_hooks.debug_statement_hook import Debug
@ -55,9 +53,7 @@ def test_non_utf8_file(tmpdir):
assert main((str(f_py),)) == 0
def test_py37_breakpoint(tmpdir, capsys):
def test_py37_breakpoint(tmpdir):
f_py = tmpdir.join('f.py')
f_py.write('def f():\n breakpoint()\n')
assert main((str(f_py),)) == 1
out, _ = capsys.readouterr()
assert out == f'{f_py}:2:4: breakpoint called\n'

View file

@ -1,5 +1,3 @@
from __future__ import annotations
import os
import subprocess
@ -7,7 +5,6 @@ import pytest
from pre_commit_hooks.destroyed_symlinks import find_destroyed_symlinks
from pre_commit_hooks.destroyed_symlinks import main
from testing.util import git_commit
TEST_SYMLINK = 'test_symlink'
TEST_SYMLINK_TARGET = '/doesnt/really/matters'
@ -26,7 +23,9 @@ def repo_with_destroyed_symlink(tmpdir):
with open(TEST_FILE, 'w') as f:
print('some random content', file=f)
subprocess.check_call(('git', 'add', '.'))
git_commit('-m', 'initial')
subprocess.check_call(
('git', 'commit', '--no-gpg-sign', '-m', 'initial'),
)
assert b'120000 ' in subprocess.check_output(
('git', 'cat-file', '-p', 'HEAD^{tree}'),
)

View file

@ -1,5 +1,3 @@
from __future__ import annotations
from unittest.mock import patch
import pytest

View file

@ -1,5 +1,3 @@
from __future__ import annotations
import pytest
from pre_commit_hooks.detect_private_key import main
@ -12,8 +10,6 @@ TESTS = (
(b'-----BEGIN OPENSSH PRIVATE KEY-----', 1),
(b'PuTTY-User-Key-File-2: ssh-rsa', 1),
(b'---- BEGIN SSH2 ENCRYPTED PRIVATE KEY ----', 1),
(b'-----BEGIN ENCRYPTED PRIVATE KEY-----', 1),
(b'-----BEGIN OpenVPN Static key V1-----', 1),
(b'ssh-rsa DATA', 0),
(b'ssh-dsa DATA', 0),
# Some arbitrary binary data

View file

@ -1,5 +1,3 @@
from __future__ import annotations
import io
import pytest

View file

@ -1,5 +1,3 @@
from __future__ import annotations
import pytest
from pre_commit_hooks.file_contents_sorter import FAIL
@ -10,9 +8,7 @@ from pre_commit_hooks.file_contents_sorter import PASS
@pytest.mark.parametrize(
('input_s', 'argv', 'expected_retval', 'output'),
(
(b'', [], PASS, b''),
(b'\n', [], FAIL, b''),
(b'\n\n', [], FAIL, b''),
(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'),
@ -67,6 +63,18 @@ from pre_commit_hooks.file_contents_sorter import PASS
FAIL,
b'Fie\nFoe\nfee\nfum\n',
),
(
b'fee\nFie\nFoe\nfum\n',
['--unique', '--ignore-case'],
PASS,
b'fee\nFie\nFoe\nfum\n',
),
(
b'fee\nfee\nFie\nFoe\nfum\n',
['--unique', '--ignore-case'],
FAIL,
b'fee\nFie\nFoe\nfum\n',
),
),
)
def test_integration(input_s, argv, expected_retval, output, tmpdir):
@ -77,24 +85,3 @@ def test_integration(input_s, argv, expected_retval, output, tmpdir):
assert path.read_binary() == output
assert output_retval == expected_retval
@pytest.mark.parametrize(
('input_s', 'argv'),
(
(
b'fee\nFie\nFoe\nfum\n',
['--unique', '--ignore-case'],
),
(
b'fee\nfee\nFie\nFoe\nfum\n',
['--unique', '--ignore-case'],
),
),
)
def test_integration_invalid_args(input_s, argv, tmpdir):
path = tmpdir.join('file.txt')
path.write_binary(input_s)
with pytest.raises(SystemExit):
main([str(path)] + argv)

View file

@ -1,5 +1,3 @@
from __future__ import annotations
from pre_commit_hooks import fix_byte_order_marker

View file

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

View file

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

View file

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

View file

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

View file

@ -1,5 +1,3 @@
from __future__ import annotations
import os
import shutil
@ -82,24 +80,6 @@ def test_autofix_main(tmpdir):
assert ret == 0
def test_invalid_main(tmpdir):
srcfile1 = tmpdir.join('not_valid_json.json')
srcfile1.write(
'{\n'
' // not json\n'
' "a": "b"\n'
'}',
)
srcfile2 = tmpdir.join('to_be_json_formatted.json')
srcfile2.write('{ "a": "b" }')
# it should have skipped the first file and formatted the second one
assert main(['--autofix', str(srcfile1), str(srcfile2)]) == 1
# confirm second file was formatted (shouldn't trigger linter again)
assert main([str(srcfile2)]) == 0
def test_orderfile_get_pretty_format():
ret = main((
'--top-keys=alist', get_resource_path('pretty_formatted_json.json'),

View file

@ -1,5 +1,3 @@
from __future__ import annotations
from pre_commit_hooks.check_yaml import yaml

View file

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

View file

@ -1,5 +1,3 @@
from __future__ import annotations
import pytest
from pre_commit_hooks.requirements_txt_fixer import FAIL
@ -68,12 +66,6 @@ from pre_commit_hooks.requirements_txt_fixer import Requirement
b'f<=2\n'
b'g<2\n',
),
(b'a==1\nb==1\na==1\n', FAIL, b'a==1\nb==1\n'),
(
b'a==1\nb==1\n#comment about a\na==1\n',
FAIL,
b'#comment about a\na==1\nb==1\n',
),
(b'ocflib\nDjango\nPyMySQL\n', FAIL, b'Django\nocflib\nPyMySQL\n'),
(
b'-e git+ssh://git_url@tag#egg=ocflib\nDjango\nPyMySQL\n',
@ -82,8 +74,6 @@ from pre_commit_hooks.requirements_txt_fixer import Requirement
),
(b'bar\npkg-resources==0.0.0\nfoo\n', FAIL, b'bar\nfoo\n'),
(b'foo\npkg-resources==0.0.0\nbar\n', FAIL, b'bar\nfoo\n'),
(b'bar\npkg_resources==0.0.0\nfoo\n', FAIL, b'bar\nfoo\n'),
(b'foo\npkg_resources==0.0.0\nbar\n', FAIL, b'bar\nfoo\n'),
(
b'git+ssh://git_url@tag#egg=ocflib\nDjango\nijk\n',
FAIL,

View file

@ -1,5 +1,3 @@
from __future__ import annotations
import os
import pytest

View file

@ -1,5 +1,3 @@
from __future__ import annotations
import textwrap
import pytest
@ -37,12 +35,6 @@ TESTS = (
1,
),
('"foo""bar"', "'foo''bar'", 1),
pytest.param(
"f'hello{\"world\"}'",
"f'hello{\"world\"}'",
0,
id='ignore nested fstrings',
),
)

View file

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

View file

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

View file

@ -1,5 +1,3 @@
from __future__ import annotations
import pytest
from pre_commit_hooks.util import CalledProcessError

View file

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