mirror of
https://github.com/pre-commit/pre-commit-hooks.git
synced 2026-04-09 12:54:17 +00:00
requirements_txt_fixer.py: Included an option to fail if no version is specified for a requirement
This commit is contained in:
parent
31903eabdb
commit
f939ecc63d
4 changed files with 165 additions and 120 deletions
|
|
@ -192,7 +192,7 @@
|
||||||
always_run: true
|
always_run: true
|
||||||
- id: requirements-txt-fixer
|
- id: requirements-txt-fixer
|
||||||
name: fix requirements.txt
|
name: fix requirements.txt
|
||||||
description: sorts entries in requirements.txt.
|
description: sorts entries in requirements.txt and checks whether a version is specified (parameterized).
|
||||||
entry: requirements-txt-fixer
|
entry: requirements-txt-fixer
|
||||||
language: python
|
language: python
|
||||||
files: (requirements|constraints).*\.txt$
|
files: (requirements|constraints).*\.txt$
|
||||||
|
|
|
||||||
|
|
@ -186,7 +186,10 @@ the following commandline options:
|
||||||
- `--top-keys comma,separated,keys` - Keys to keep at the top of mappings.
|
- `--top-keys comma,separated,keys` - Keys to keep at the top of mappings.
|
||||||
|
|
||||||
#### `requirements-txt-fixer`
|
#### `requirements-txt-fixer`
|
||||||
Sorts entries in requirements.txt and constraints.txt and removes incorrect entry for `pkg-resources==0.0.0`
|
Sorts entries in requirements.txt and constraints.txt and removes incorrect entry for `pkg-resources==0.0.0`
|
||||||
|
Provides also an optional check if a version is specified for each requirement. You can configure this with
|
||||||
|
the following commandline options:
|
||||||
|
- `--fail-without-version` - Fails when no version is specified for a requirement
|
||||||
|
|
||||||
#### `sort-simple-yaml`
|
#### `sort-simple-yaml`
|
||||||
Sorts simple YAML files which consist only of top-level
|
Sorts simple YAML files which consist only of top-level
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ FAIL = 1
|
||||||
class Requirement:
|
class Requirement:
|
||||||
UNTIL_COMPARISON = re.compile(b'={2,3}|!=|~=|>=?|<=?')
|
UNTIL_COMPARISON = re.compile(b'={2,3}|!=|~=|>=?|<=?')
|
||||||
UNTIL_SEP = re.compile(rb'[^;\s]+')
|
UNTIL_SEP = re.compile(rb'[^;\s]+')
|
||||||
|
VERSION_SPECIFIED = re.compile(b'.+(={2,3}|!=|~=|>=?|<=?).+')
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.value: bytes | None = None
|
self.value: bytes | None = None
|
||||||
|
|
@ -58,6 +59,9 @@ class Requirement:
|
||||||
not self.value.rstrip(b'\r\n').endswith(b'\\')
|
not self.value.rstrip(b'\r\n').endswith(b'\\')
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def contains_version_specifier(self) -> bool:
|
||||||
|
return bool(self.VERSION_SPECIFIED.match(self.value))
|
||||||
|
|
||||||
def append_value(self, value: bytes) -> None:
|
def append_value(self, value: bytes) -> None:
|
||||||
if self.value is not None:
|
if self.value is not None:
|
||||||
self.value += value
|
self.value += value
|
||||||
|
|
@ -65,7 +69,7 @@ class Requirement:
|
||||||
self.value = value
|
self.value = value
|
||||||
|
|
||||||
|
|
||||||
def fix_requirements(f: IO[bytes]) -> int:
|
def fix_requirements(f: IO[bytes], fail_without_version: bool) -> int:
|
||||||
requirements: list[Requirement] = []
|
requirements: list[Requirement] = []
|
||||||
before = list(f)
|
before = list(f)
|
||||||
after: list[bytes] = []
|
after: list[bytes] = []
|
||||||
|
|
@ -121,6 +125,17 @@ def fix_requirements(f: IO[bytes]) -> int:
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# check for requirements without a version specified
|
||||||
|
if fail_without_version:
|
||||||
|
missing_requirement_found = False
|
||||||
|
for req in requirements:
|
||||||
|
if not req.contains_version_specifier():
|
||||||
|
print(f'Missing version for requirement {req.name.decode()}')
|
||||||
|
missing_requirement_found = True
|
||||||
|
|
||||||
|
if missing_requirement_found:
|
||||||
|
return FAIL
|
||||||
|
|
||||||
# sort the requirements and remove duplicates
|
# sort the requirements and remove duplicates
|
||||||
prev = None
|
prev = None
|
||||||
for requirement in sorted(requirements):
|
for requirement in sorted(requirements):
|
||||||
|
|
@ -145,13 +160,15 @@ def fix_requirements(f: IO[bytes]) -> int:
|
||||||
def main(argv: Sequence[str] | None = None) -> int:
|
def main(argv: Sequence[str] | None = None) -> int:
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument('filenames', nargs='*', help='Filenames to fix')
|
parser.add_argument('filenames', nargs='*', help='Filenames to fix')
|
||||||
|
parser.add_argument("--fail-without-version", action="store_true",
|
||||||
|
help="Fail if a requirement is missing a version")
|
||||||
args = parser.parse_args(argv)
|
args = parser.parse_args(argv)
|
||||||
|
|
||||||
retv = PASS
|
retv = PASS
|
||||||
|
|
||||||
for arg in args.filenames:
|
for arg in args.filenames:
|
||||||
with open(arg, 'rb+') as file_obj:
|
with open(arg, 'rb+') as file_obj:
|
||||||
ret_for_file = fix_requirements(file_obj)
|
ret_for_file = fix_requirements(file_obj, args.fail_without_version)
|
||||||
|
|
||||||
if ret_for_file:
|
if ret_for_file:
|
||||||
print(f'Sorting {arg}')
|
print(f'Sorting {arg}')
|
||||||
|
|
|
||||||
|
|
@ -9,130 +9,155 @@ from pre_commit_hooks.requirements_txt_fixer import Requirement
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
('input_s', 'expected_retval', 'output'),
|
('input_s', 'argv', 'expected_retval', 'output'),
|
||||||
(
|
(
|
||||||
(b'', PASS, b''),
|
|
||||||
(b'\n', PASS, b'\n'),
|
(b'', [], PASS, b''),
|
||||||
(b'# intentionally empty\n', PASS, b'# intentionally empty\n'),
|
(b'\n', [], PASS, b'\n'),
|
||||||
(b'foo\n# comment at end\n', PASS, b'foo\n# comment at end\n'),
|
(b'# intentionally empty\n', [], PASS, b'# intentionally empty\n'),
|
||||||
(b'foo\nbar\n', FAIL, b'bar\nfoo\n'),
|
(b'foo\n# comment at end\n', [], PASS, b'foo\n# comment at end\n'),
|
||||||
(b'bar\nfoo\n', PASS, b'bar\nfoo\n'),
|
(b'foo\nbar\n', [], FAIL, b'bar\nfoo\n'),
|
||||||
(b'a\nc\nb\n', FAIL, b'a\nb\nc\n'),
|
(b'bar\nfoo\n', [], PASS, b'bar\nfoo\n'),
|
||||||
(b'a\nc\nb', FAIL, b'a\nb\nc\n'),
|
(b'a\nc\nb\n', [], FAIL, b'a\nb\nc\n'),
|
||||||
(b'a\nb\nc', FAIL, b'a\nb\nc\n'),
|
(b'a\nc\nb', [], FAIL, b'a\nb\nc\n'),
|
||||||
(
|
(b'a\nb\nc', [], FAIL, b'a\nb\nc\n'),
|
||||||
b'#comment1\nfoo\n#comment2\nbar\n',
|
(
|
||||||
FAIL,
|
b'#comment1\nfoo\n#comment2\nbar\n',
|
||||||
b'#comment2\nbar\n#comment1\nfoo\n',
|
[],
|
||||||
),
|
FAIL,
|
||||||
(
|
b'#comment2\nbar\n#comment1\nfoo\n',
|
||||||
b'#comment1\nbar\n#comment2\nfoo\n',
|
),
|
||||||
PASS,
|
(
|
||||||
b'#comment1\nbar\n#comment2\nfoo\n',
|
b'#comment1\nbar\n#comment2\nfoo\n',
|
||||||
),
|
[],
|
||||||
(b'#comment\n\nfoo\nbar\n', FAIL, b'#comment\n\nbar\nfoo\n'),
|
PASS,
|
||||||
(b'#comment\n\nbar\nfoo\n', PASS, b'#comment\n\nbar\nfoo\n'),
|
b'#comment1\nbar\n#comment2\nfoo\n',
|
||||||
(
|
),
|
||||||
b'foo\n\t#comment with indent\nbar\n',
|
(b'#comment\n\nfoo\nbar\n', [], FAIL, b'#comment\n\nbar\nfoo\n'),
|
||||||
FAIL,
|
(b'#comment\n\nbar\nfoo\n', [], PASS, b'#comment\n\nbar\nfoo\n'),
|
||||||
b'\t#comment with indent\nbar\nfoo\n',
|
(
|
||||||
),
|
b'foo\n\t#comment with indent\nbar\n',
|
||||||
(
|
[],
|
||||||
b'bar\n\t#comment with indent\nfoo\n',
|
FAIL,
|
||||||
PASS,
|
b'\t#comment with indent\nbar\nfoo\n',
|
||||||
b'bar\n\t#comment with indent\nfoo\n',
|
),
|
||||||
),
|
(
|
||||||
(b'\nfoo\nbar\n', FAIL, b'bar\n\nfoo\n'),
|
b'bar\n\t#comment with indent\nfoo\n',
|
||||||
(b'\nbar\nfoo\n', PASS, b'\nbar\nfoo\n'),
|
[],
|
||||||
(
|
PASS,
|
||||||
b'pyramid-foo==1\npyramid>=2\n',
|
b'bar\n\t#comment with indent\nfoo\n',
|
||||||
FAIL,
|
),
|
||||||
b'pyramid>=2\npyramid-foo==1\n',
|
(b'\nfoo\nbar\n', [], FAIL, b'bar\n\nfoo\n'),
|
||||||
),
|
(b'\nbar\nfoo\n', [], PASS, b'\nbar\nfoo\n'),
|
||||||
(
|
(
|
||||||
b'a==1\n'
|
b'pyramid-foo==1\npyramid>=2\n',
|
||||||
b'c>=1\n'
|
[],
|
||||||
b'bbbb!=1\n'
|
FAIL,
|
||||||
b'c-a>=1;python_version>="3.6"\n'
|
b'pyramid>=2\npyramid-foo==1\n',
|
||||||
b'e>=2\n'
|
),
|
||||||
b'd>2\n'
|
(
|
||||||
b'g<2\n'
|
b'a==1\n'
|
||||||
b'f<=2\n',
|
b'c>=1\n'
|
||||||
FAIL,
|
b'bbbb!=1\n'
|
||||||
b'a==1\n'
|
b'c-a>=1;python_version>="3.6"\n'
|
||||||
b'bbbb!=1\n'
|
b'e>=2\n'
|
||||||
b'c>=1\n'
|
b'd>2\n'
|
||||||
b'c-a>=1;python_version>="3.6"\n'
|
b'g<2\n'
|
||||||
b'd>2\n'
|
b'f<=2\n',
|
||||||
b'e>=2\n'
|
[],
|
||||||
b'f<=2\n'
|
FAIL,
|
||||||
b'g<2\n',
|
b'a==1\n'
|
||||||
),
|
b'bbbb!=1\n'
|
||||||
(b'a==1\nb==1\na==1\n', FAIL, b'a==1\nb==1\n'),
|
b'c>=1\n'
|
||||||
(
|
b'c-a>=1;python_version>="3.6"\n'
|
||||||
b'a==1\nb==1\n#comment about a\na==1\n',
|
b'd>2\n'
|
||||||
FAIL,
|
b'e>=2\n'
|
||||||
b'#comment about a\na==1\nb==1\n',
|
b'f<=2\n'
|
||||||
),
|
b'g<2\n',
|
||||||
(b'ocflib\nDjango\nPyMySQL\n', FAIL, b'Django\nocflib\nPyMySQL\n'),
|
),
|
||||||
(
|
(b'a==1\nb==1\na==1\n', [], FAIL, b'a==1\nb==1\n'),
|
||||||
b'-e git+ssh://git_url@tag#egg=ocflib\nDjango\nPyMySQL\n',
|
(
|
||||||
FAIL,
|
b'a==1\nb==1\n#comment about a\na==1\n',
|
||||||
b'Django\n-e git+ssh://git_url@tag#egg=ocflib\nPyMySQL\n',
|
[],
|
||||||
),
|
FAIL,
|
||||||
(b'bar\npkg-resources==0.0.0\nfoo\n', FAIL, b'bar\nfoo\n'),
|
b'#comment about a\na==1\nb==1\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'ocflib\nDjango\nPyMySQL\n', [], FAIL, b'Django\nocflib\nPyMySQL\n'),
|
||||||
(b'foo\npkg_resources==0.0.0\nbar\n', FAIL, b'bar\nfoo\n'),
|
(
|
||||||
(
|
b'-e git+ssh://git_url@tag#egg=ocflib\nDjango\nPyMySQL\n',
|
||||||
b'git+ssh://git_url@tag#egg=ocflib\nDjango\nijk\n',
|
[],
|
||||||
FAIL,
|
FAIL,
|
||||||
b'Django\nijk\ngit+ssh://git_url@tag#egg=ocflib\n',
|
b'Django\n-e git+ssh://git_url@tag#egg=ocflib\nPyMySQL\n',
|
||||||
),
|
),
|
||||||
(
|
(b'bar\npkg-resources==0.0.0\nfoo\n', [], FAIL, b'bar\nfoo\n'),
|
||||||
b'b==1.0.0\n'
|
(b'foo\npkg-resources==0.0.0\nbar\n', [], FAIL, b'bar\nfoo\n'),
|
||||||
b'c=2.0.0 \\\n'
|
(b'bar\npkg_resources==0.0.0\nfoo\n', [], FAIL, b'bar\nfoo\n'),
|
||||||
b' --hash=sha256:abcd\n'
|
(b'foo\npkg_resources==0.0.0\nbar\n', [], FAIL, b'bar\nfoo\n'),
|
||||||
b'a=3.0.0 \\\n'
|
(
|
||||||
b' --hash=sha256:a1b1c1d1',
|
b'git+ssh://git_url@tag#egg=ocflib\nDjango\nijk\n',
|
||||||
FAIL,
|
[],
|
||||||
b'a=3.0.0 \\\n'
|
FAIL,
|
||||||
b' --hash=sha256:a1b1c1d1\n'
|
b'Django\nijk\ngit+ssh://git_url@tag#egg=ocflib\n',
|
||||||
b'b==1.0.0\n'
|
),
|
||||||
b'c=2.0.0 \\\n'
|
(
|
||||||
b' --hash=sha256:abcd\n',
|
b'b==1.0.0\n'
|
||||||
),
|
b'c=2.0.0 \\\n'
|
||||||
(
|
b' --hash=sha256:abcd\n'
|
||||||
b'a=2.0.0 \\\n --hash=sha256:abcd\nb==1.0.0\n',
|
b'a=3.0.0 \\\n'
|
||||||
PASS,
|
b' --hash=sha256:a1b1c1d1',
|
||||||
b'a=2.0.0 \\\n --hash=sha256:abcd\nb==1.0.0\n',
|
[],
|
||||||
),
|
FAIL,
|
||||||
|
b'a=3.0.0 \\\n'
|
||||||
|
b' --hash=sha256:a1b1c1d1\n'
|
||||||
|
b'b==1.0.0\n'
|
||||||
|
b'c=2.0.0 \\\n'
|
||||||
|
b' --hash=sha256:abcd\n',
|
||||||
|
),
|
||||||
|
(
|
||||||
|
b'a=2.0.0 \\\n --hash=sha256:abcd\nb==1.0.0\n',
|
||||||
|
[],
|
||||||
|
PASS,
|
||||||
|
b'a=2.0.0 \\\n --hash=sha256:abcd\nb==1.0.0\n',
|
||||||
|
),
|
||||||
|
(b'bar\nfoo\n', ["--fail-without-version"], FAIL, b'bar\nfoo\n'),
|
||||||
|
(b'bar==1.0\nfoo==1.1a\n', ["--fail-without-version"], PASS, b'bar==1.0\nfoo==1.1a\n'),
|
||||||
|
(b'#test\nbar==1.0\nfoo==1.1a\n', ["--fail-without-version"], PASS, b'#test\nbar==1.0\nfoo==1.1a\n'),
|
||||||
|
(b'bar==1.0\n#test\nfoo==1.1a\n', ["--fail-without-version"], PASS, b'bar==1.0\n#test\nfoo==1.1a\n'),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test_integration(input_s, expected_retval, output, tmpdir):
|
def test_integration(input_s, argv, expected_retval, output, tmpdir):
|
||||||
path = tmpdir.join('file.txt')
|
path = tmpdir.join('file.txt')
|
||||||
path.write_binary(input_s)
|
path.write_binary(input_s)
|
||||||
|
|
||||||
output_retval = main([str(path)])
|
output_retval = main([str(path)] + argv)
|
||||||
|
|
||||||
assert path.read_binary() == output
|
assert path.read_binary() == output
|
||||||
assert output_retval == expected_retval
|
assert output_retval == expected_retval
|
||||||
|
|
||||||
|
|
||||||
def test_requirement_object():
|
def test_requirement_object():
|
||||||
top_of_file = Requirement()
|
top_of_file = Requirement()
|
||||||
top_of_file.comments.append(b'#foo')
|
top_of_file.comments.append(b'#foo')
|
||||||
top_of_file.value = b'\n'
|
top_of_file.value = b'\n'
|
||||||
|
|
||||||
requirement_foo = Requirement()
|
requirement_foo = Requirement()
|
||||||
requirement_foo.value = b'foo'
|
requirement_foo.value = b'foo'
|
||||||
|
|
||||||
requirement_bar = Requirement()
|
requirement_bar = Requirement()
|
||||||
requirement_bar.value = b'bar'
|
requirement_bar.value = b'bar'
|
||||||
|
|
||||||
# This may look redundant, but we need to test both foo.__lt__(bar) and
|
requirements_bar_versioned = Requirement()
|
||||||
# bar.__lt__(foo)
|
requirements_bar_versioned.value = b'bar==1.0'
|
||||||
assert requirement_foo > top_of_file
|
|
||||||
assert top_of_file < requirement_foo
|
# check for version specification
|
||||||
assert requirement_foo > requirement_bar
|
assert top_of_file.contains_version_specifier() is False
|
||||||
assert requirement_bar < requirement_foo
|
assert requirement_foo.contains_version_specifier() is False
|
||||||
|
assert requirement_bar.contains_version_specifier() is False
|
||||||
|
assert requirements_bar_versioned.contains_version_specifier() is True
|
||||||
|
|
||||||
|
# This may look redundant, but we need to test both foo.__lt__(bar) and
|
||||||
|
# bar.__lt__(foo)
|
||||||
|
assert requirement_foo > top_of_file
|
||||||
|
assert top_of_file < requirement_foo
|
||||||
|
assert requirement_foo > requirement_bar
|
||||||
|
assert requirement_bar < requirement_foo
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue