Skip to content

Commit ad4b2b1

Browse files
committed
Add check for illegal Widnows names
Fixes #589
1 parent 6afc574 commit ad4b2b1

File tree

5 files changed

+151
-0
lines changed

5 files changed

+151
-0
lines changed

.pre-commit-hooks.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@
4040
language: python
4141
types: [text, executable]
4242
stages: [commit, push, manual]
43+
- id: check-illegal-windows-names
44+
name: Check for illegal Windows names
45+
description: Check for files that cannot be created on Windows.
46+
entry: check-illegal-windows-names
47+
language: python
4348
- id: check-json
4449
name: check json
4550
description: checks json files for parseable syntax.

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@ Checks for a common error of placing code before the docstring.
5151
#### `check-executables-have-shebangs`
5252
Checks that non-binary executables have a proper shebang.
5353

54+
#### `check-illegal-windows-names`
55+
Check for files that cannot be created on Windows.
56+
5457
#### `check-json`
5558
Attempts to load all json files to verify syntax.
5659

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import argparse
2+
import os.path
3+
from pathlib import Path
4+
from typing import Iterable, Iterator, Optional, Sequence, Set
5+
6+
from pre_commit_hooks.util import added_files
7+
8+
9+
def lower_set(iterable: Iterable[str]) -> Set[str]:
10+
return {x.lower() for x in iterable}
11+
12+
13+
def parents(file: str) -> Iterator[str]:
14+
file = os.path.dirname(file)
15+
while file:
16+
yield file
17+
file = os.path.dirname(file)
18+
19+
20+
def directories_for(files: Set[str]) -> Set[str]:
21+
return {parent for file in files for parent in parents(file)}
22+
23+
24+
# https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file
25+
ILLEGAL_NAMES = {
26+
"CON",
27+
"PRN",
28+
"AUX",
29+
"NUL",
30+
*(f"COM{i}" for i in range(1, 10)),
31+
*(f"LPT{i}" for i in range(1, 10)),
32+
}
33+
34+
35+
def find_illegal_windows_names(filenames: Sequence[str]) -> int:
36+
relevant_files = set(filenames) | added_files()
37+
relevant_files |= directories_for(relevant_files)
38+
retv = 0
39+
40+
for filename in relevant_files:
41+
root = Path(filename)
42+
while root.suffix:
43+
root = root.with_suffix("")
44+
if root.name.lower() in lower_set(ILLEGAL_NAMES):
45+
print(f"Illegal name {filename}")
46+
retv = 1
47+
48+
return retv
49+
50+
51+
def main(argv: Optional[Sequence[str]] = None) -> int:
52+
parser = argparse.ArgumentParser()
53+
parser.add_argument(
54+
"filenames",
55+
nargs="*",
56+
help="Filenames pre-commit believes are changed.",
57+
)
58+
59+
args = parser.parse_args(argv)
60+
return find_illegal_windows_names(args.filenames)
61+
62+
63+
if __name__ == "__main__":
64+
exit(main())

setup.cfg

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ console_scripts =
3737
check-case-conflict = pre_commit_hooks.check_case_conflict:main
3838
check-docstring-first = pre_commit_hooks.check_docstring_first:main
3939
check-executables-have-shebangs = pre_commit_hooks.check_executables_have_shebangs:main
40+
check-illegal-windows-names = pre_commit_hooks.check_illegal_windows_names:main
4041
check-json = pre_commit_hooks.check_json:main
4142
check-merge-conflict = pre_commit_hooks.check_merge_conflict:main
4243
check-shebang-scripts-are-executable = pre_commit_hooks.check_shebang_scripts_are_executable:main
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import sys
2+
3+
import pytest
4+
5+
from pre_commit_hooks.check_illegal_windows_names import (
6+
find_illegal_windows_names,
7+
main,
8+
parents,
9+
)
10+
from pre_commit_hooks.util import cmd_output
11+
12+
skip_win32 = pytest.mark.skipif(
13+
sys.platform == "win32",
14+
reason="case conflicts between directories and files",
15+
)
16+
17+
18+
def test_parents():
19+
assert set(parents("a")) == set()
20+
assert set(parents("a/b")) == {"a"}
21+
assert set(parents("a/b/c")) == {"a/b", "a"}
22+
assert set(parents("a/b/c/d")) == {"a/b/c", "a/b", "a"}
23+
24+
25+
def test_nothing_added(temp_git_dir):
26+
with temp_git_dir.as_cwd():
27+
assert find_illegal_windows_names(["f.py"]) == 0
28+
29+
30+
def test_adding_something(temp_git_dir):
31+
with temp_git_dir.as_cwd():
32+
temp_git_dir.join("f.py").write("print('hello world')")
33+
cmd_output("git", "add", "f.py")
34+
35+
assert find_illegal_windows_names(["f.py"]) == 0
36+
37+
38+
@skip_win32 # pragma: win32 no cover
39+
def test_adding_something_with_illegal_filename(temp_git_dir):
40+
with temp_git_dir.as_cwd():
41+
temp_git_dir.join("CoM3.py").write("print('hello world')")
42+
cmd_output("git", "add", "CoM3.py")
43+
44+
assert find_illegal_windows_names(["CoM3.py"]) == 1
45+
46+
47+
@skip_win32 # pragma: win32 no cover
48+
def test_adding_files_with_illegal_directory(temp_git_dir):
49+
with temp_git_dir.as_cwd():
50+
temp_git_dir.mkdir("lpt2").join("x").write("foo")
51+
cmd_output("git", "add", "-A")
52+
53+
assert find_illegal_windows_names([]) == 1
54+
55+
56+
@skip_win32 # pragma: win32 no cover
57+
def test_adding_files_with_illegal_deep_directories(temp_git_dir):
58+
with temp_git_dir.as_cwd():
59+
temp_git_dir.mkdir("x").mkdir("y").join("pRn").write("foo")
60+
cmd_output("git", "add", "-A")
61+
62+
assert find_illegal_windows_names([]) == 1
63+
64+
65+
@skip_win32 # pragma: win32 no cover
66+
def test_integration(temp_git_dir):
67+
with temp_git_dir.as_cwd():
68+
assert main(argv=[]) == 0
69+
70+
temp_git_dir.join("f.py").write("print('hello world')")
71+
cmd_output("git", "add", "f.py")
72+
73+
assert main(argv=["f.py"]) == 0
74+
75+
temp_git_dir.join("CON.py").write("print('hello world')")
76+
cmd_output("git", "add", "CON.py")
77+
78+
assert main(argv=["CON.py"]) == 1

0 commit comments

Comments
 (0)