From f939ecc63d921d9e3e1865480b3333c2bc3658b5 Mon Sep 17 00:00:00 2001 From: Yannik Henke <3532075+binka-dev@users.noreply.github.com> Date: Wed, 5 Mar 2025 10:11:29 +0100 Subject: [PATCH 1/4] requirements_txt_fixer.py: Included an option to fail if no version is specified for a requirement --- .pre-commit-hooks.yaml | 2 +- README.md | 5 +- pre_commit_hooks/requirements_txt_fixer.py | 21 +- tests/requirements_txt_fixer_test.py | 257 +++++++++++---------- 4 files changed, 165 insertions(+), 120 deletions(-) diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml index b71169bb..278331e0 100644 --- a/.pre-commit-hooks.yaml +++ b/.pre-commit-hooks.yaml @@ -192,7 +192,7 @@ always_run: true - id: requirements-txt-fixer 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 language: python files: (requirements|constraints).*\.txt$ diff --git a/README.md b/README.md index c0f678fd..4c37b6a9 100644 --- a/README.md +++ b/README.md @@ -186,7 +186,10 @@ 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 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` Sorts simple YAML files which consist only of top-level diff --git a/pre_commit_hooks/requirements_txt_fixer.py b/pre_commit_hooks/requirements_txt_fixer.py index 8ce8ec64..3648dbd3 100644 --- a/pre_commit_hooks/requirements_txt_fixer.py +++ b/pre_commit_hooks/requirements_txt_fixer.py @@ -13,6 +13,7 @@ class Requirement: UNTIL_COMPARISON = re.compile(b'={2,3}|!=|~=|>=?|<=?') UNTIL_SEP = re.compile(rb'[^;\s]+') + VERSION_SPECIFIED = re.compile(b'.+(={2,3}|!=|~=|>=?|<=?).+') def __init__(self) -> None: self.value: bytes | None = None @@ -58,6 +59,9 @@ def is_complete(self) -> bool: 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: if self.value is not None: self.value += value @@ -65,7 +69,7 @@ def append_value(self, value: bytes) -> None: self.value = value -def fix_requirements(f: IO[bytes]) -> int: +def fix_requirements(f: IO[bytes], fail_without_version: bool) -> int: requirements: list[Requirement] = [] before = list(f) 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 prev = None for requirement in sorted(requirements): @@ -145,13 +160,15 @@ def fix_requirements(f: IO[bytes]) -> int: def main(argv: Sequence[str] | None = None) -> int: parser = argparse.ArgumentParser() 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) retv = PASS for arg in args.filenames: 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: print(f'Sorting {arg}') diff --git a/tests/requirements_txt_fixer_test.py b/tests/requirements_txt_fixer_test.py index c0d2c65d..b06d38fa 100644 --- a/tests/requirements_txt_fixer_test.py +++ b/tests/requirements_txt_fixer_test.py @@ -9,130 +9,155 @@ @pytest.mark.parametrize( - ('input_s', 'expected_retval', 'output'), + ('input_s', 'argv', 'expected_retval', 'output'), ( - (b'', PASS, b''), - (b'\n', PASS, b'\n'), - (b'# intentionally empty\n', PASS, b'# intentionally empty\n'), - (b'foo\n# comment at end\n', PASS, b'foo\n# comment at end\n'), - (b'foo\nbar\n', FAIL, b'bar\nfoo\n'), - (b'bar\nfoo\n', PASS, b'bar\nfoo\n'), - (b'a\nc\nb\n', 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'#comment2\nbar\n#comment1\nfoo\n', - ), - ( - b'#comment1\nbar\n#comment2\nfoo\n', - PASS, - b'#comment1\nbar\n#comment2\nfoo\n', - ), - (b'#comment\n\nfoo\nbar\n', FAIL, b'#comment\n\nbar\nfoo\n'), - (b'#comment\n\nbar\nfoo\n', PASS, b'#comment\n\nbar\nfoo\n'), - ( - b'foo\n\t#comment with indent\nbar\n', - FAIL, - b'\t#comment with indent\nbar\nfoo\n', - ), - ( - b'bar\n\t#comment with indent\nfoo\n', - PASS, - b'bar\n\t#comment with indent\nfoo\n', - ), - (b'\nfoo\nbar\n', FAIL, b'bar\n\nfoo\n'), - (b'\nbar\nfoo\n', PASS, b'\nbar\nfoo\n'), - ( - b'pyramid-foo==1\npyramid>=2\n', - FAIL, - b'pyramid>=2\npyramid-foo==1\n', - ), - ( - b'a==1\n' - b'c>=1\n' - b'bbbb!=1\n' - b'c-a>=1;python_version>="3.6"\n' - b'e>=2\n' - b'd>2\n' - b'g<2\n' - b'f<=2\n', - FAIL, - b'a==1\n' - b'bbbb!=1\n' - b'c>=1\n' - b'c-a>=1;python_version>="3.6"\n' - b'd>2\n' - b'e>=2\n' - b'f<=2\n' - b'g<2\n', - ), - (b'a==1\nb==1\na==1\n', FAIL, b'a==1\nb==1\n'), - ( - b'a==1\nb==1\n#comment about a\na==1\n', - FAIL, - b'#comment about a\na==1\nb==1\n', - ), - (b'ocflib\nDjango\nPyMySQL\n', FAIL, b'Django\nocflib\nPyMySQL\n'), - ( - b'-e git+ssh://git_url@tag#egg=ocflib\nDjango\nPyMySQL\n', - FAIL, - b'Django\n-e git+ssh://git_url@tag#egg=ocflib\nPyMySQL\n', - ), - (b'bar\npkg-resources==0.0.0\nfoo\n', FAIL, b'bar\nfoo\n'), - (b'foo\npkg-resources==0.0.0\nbar\n', FAIL, b'bar\nfoo\n'), - (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, - b'Django\nijk\ngit+ssh://git_url@tag#egg=ocflib\n', - ), - ( - b'b==1.0.0\n' - b'c=2.0.0 \\\n' - b' --hash=sha256:abcd\n' - b'a=3.0.0 \\\n' - b' --hash=sha256:a1b1c1d1', - FAIL, - b'a=3.0.0 \\\n' - b' --hash=sha256:a1b1c1d1\n' - b'b==1.0.0\n' - b'c=2.0.0 \\\n' - b' --hash=sha256:abcd\n', - ), - ( - b'a=2.0.0 \\\n --hash=sha256:abcd\nb==1.0.0\n', - PASS, - b'a=2.0.0 \\\n --hash=sha256:abcd\nb==1.0.0\n', - ), + + (b'', [], PASS, b''), + (b'\n', [], PASS, b'\n'), + (b'# intentionally empty\n', [], PASS, b'# intentionally empty\n'), + (b'foo\n# comment at end\n', [], PASS, b'foo\n# comment at end\n'), + (b'foo\nbar\n', [], FAIL, b'bar\nfoo\n'), + (b'bar\nfoo\n', [], PASS, b'bar\nfoo\n'), + (b'a\nc\nb\n', [], 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'#comment2\nbar\n#comment1\nfoo\n', + ), + ( + b'#comment1\nbar\n#comment2\nfoo\n', + [], + PASS, + b'#comment1\nbar\n#comment2\nfoo\n', + ), + (b'#comment\n\nfoo\nbar\n', [], FAIL, b'#comment\n\nbar\nfoo\n'), + (b'#comment\n\nbar\nfoo\n', [], PASS, b'#comment\n\nbar\nfoo\n'), + ( + b'foo\n\t#comment with indent\nbar\n', + [], + FAIL, + b'\t#comment with indent\nbar\nfoo\n', + ), + ( + b'bar\n\t#comment with indent\nfoo\n', + [], + PASS, + b'bar\n\t#comment with indent\nfoo\n', + ), + (b'\nfoo\nbar\n', [], FAIL, b'bar\n\nfoo\n'), + (b'\nbar\nfoo\n', [], PASS, b'\nbar\nfoo\n'), + ( + b'pyramid-foo==1\npyramid>=2\n', + [], + FAIL, + b'pyramid>=2\npyramid-foo==1\n', + ), + ( + b'a==1\n' + b'c>=1\n' + b'bbbb!=1\n' + b'c-a>=1;python_version>="3.6"\n' + b'e>=2\n' + b'd>2\n' + b'g<2\n' + b'f<=2\n', + [], + FAIL, + b'a==1\n' + b'bbbb!=1\n' + b'c>=1\n' + b'c-a>=1;python_version>="3.6"\n' + b'd>2\n' + b'e>=2\n' + b'f<=2\n' + b'g<2\n', + ), + (b'a==1\nb==1\na==1\n', [], FAIL, b'a==1\nb==1\n'), + ( + b'a==1\nb==1\n#comment about a\na==1\n', + [], + FAIL, + b'#comment about a\na==1\nb==1\n', + ), + (b'ocflib\nDjango\nPyMySQL\n', [], FAIL, b'Django\nocflib\nPyMySQL\n'), + ( + b'-e git+ssh://git_url@tag#egg=ocflib\nDjango\nPyMySQL\n', + [], + FAIL, + b'Django\n-e git+ssh://git_url@tag#egg=ocflib\nPyMySQL\n', + ), + (b'bar\npkg-resources==0.0.0\nfoo\n', [], FAIL, b'bar\nfoo\n'), + (b'foo\npkg-resources==0.0.0\nbar\n', [], FAIL, b'bar\nfoo\n'), + (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, + b'Django\nijk\ngit+ssh://git_url@tag#egg=ocflib\n', + ), + ( + b'b==1.0.0\n' + b'c=2.0.0 \\\n' + b' --hash=sha256:abcd\n' + b'a=3.0.0 \\\n' + b' --hash=sha256:a1b1c1d1', + [], + FAIL, + b'a=3.0.0 \\\n' + b' --hash=sha256:a1b1c1d1\n' + b'b==1.0.0\n' + b'c=2.0.0 \\\n' + b' --hash=sha256:abcd\n', + ), + ( + b'a=2.0.0 \\\n --hash=sha256:abcd\nb==1.0.0\n', + [], + PASS, + b'a=2.0.0 \\\n --hash=sha256:abcd\nb==1.0.0\n', + ), + (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): - path = tmpdir.join('file.txt') - path.write_binary(input_s) +def test_integration(input_s, argv, expected_retval, output, tmpdir): + path = tmpdir.join('file.txt') + path.write_binary(input_s) - output_retval = main([str(path)]) + output_retval = main([str(path)] + argv) - assert path.read_binary() == output - assert output_retval == expected_retval + assert path.read_binary() == output + assert output_retval == expected_retval def test_requirement_object(): - top_of_file = Requirement() - top_of_file.comments.append(b'#foo') - top_of_file.value = b'\n' + top_of_file = Requirement() + top_of_file.comments.append(b'#foo') + top_of_file.value = b'\n' + + requirement_foo = Requirement() + requirement_foo.value = b'foo' + + requirement_bar = Requirement() + requirement_bar.value = b'bar' - requirement_foo = Requirement() - requirement_foo.value = b'foo' + requirements_bar_versioned = Requirement() + requirements_bar_versioned.value = b'bar==1.0' - requirement_bar = Requirement() - requirement_bar.value = b'bar' + # check for version specification + assert top_of_file.contains_version_specifier() is False + 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 + # 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 From fb0f6fac81c2150e4e5ce64b88114e50803663c7 Mon Sep 17 00:00:00 2001 From: Yannik Henke <3532075+binka-dev@users.noreply.github.com> Date: Wed, 5 Mar 2025 10:19:52 +0100 Subject: [PATCH 2/4] improved error message --- pre_commit_hooks/requirements_txt_fixer.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pre_commit_hooks/requirements_txt_fixer.py b/pre_commit_hooks/requirements_txt_fixer.py index 3648dbd3..0424b5ef 100644 --- a/pre_commit_hooks/requirements_txt_fixer.py +++ b/pre_commit_hooks/requirements_txt_fixer.py @@ -151,6 +151,7 @@ def fix_requirements(f: IO[bytes], fail_without_version: bool) -> int: if before_string == after_string: return PASS else: + print("Sorting requirements") f.seek(0) f.write(after_string) f.truncate() @@ -171,7 +172,7 @@ def main(argv: Sequence[str] | None = None) -> int: ret_for_file = fix_requirements(file_obj, args.fail_without_version) if ret_for_file: - print(f'Sorting {arg}') + print(f'Error in file {arg}') retv |= ret_for_file From df46f719801cb409e57ae9ebc36f677eda07ac73 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 5 Mar 2025 09:41:58 +0000 Subject: [PATCH 3/4] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- README.md | 2 +- pre_commit_hooks/requirements_txt_fixer.py | 8 +- tests/requirements_txt_fixer_test.py | 272 ++++++++++----------- 3 files changed, 142 insertions(+), 140 deletions(-) diff --git a/README.md b/README.md index 4c37b6a9..74e2425b 100644 --- a/README.md +++ b/README.md @@ -186,7 +186,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 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 diff --git a/pre_commit_hooks/requirements_txt_fixer.py b/pre_commit_hooks/requirements_txt_fixer.py index 0424b5ef..8e97bcf1 100644 --- a/pre_commit_hooks/requirements_txt_fixer.py +++ b/pre_commit_hooks/requirements_txt_fixer.py @@ -151,7 +151,7 @@ def fix_requirements(f: IO[bytes], fail_without_version: bool) -> int: if before_string == after_string: return PASS else: - print("Sorting requirements") + print('Sorting requirements') f.seek(0) f.write(after_string) f.truncate() @@ -161,8 +161,10 @@ def fix_requirements(f: IO[bytes], fail_without_version: bool) -> int: def main(argv: Sequence[str] | None = None) -> int: parser = argparse.ArgumentParser() 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") + parser.add_argument( + '--fail-without-version', action='store_true', + help='Fail if a requirement is missing a version', + ) args = parser.parse_args(argv) retv = PASS diff --git a/tests/requirements_txt_fixer_test.py b/tests/requirements_txt_fixer_test.py index b06d38fa..b12f2f39 100644 --- a/tests/requirements_txt_fixer_test.py +++ b/tests/requirements_txt_fixer_test.py @@ -12,152 +12,152 @@ ('input_s', 'argv', 'expected_retval', 'output'), ( - (b'', [], PASS, b''), - (b'\n', [], PASS, b'\n'), - (b'# intentionally empty\n', [], PASS, b'# intentionally empty\n'), - (b'foo\n# comment at end\n', [], PASS, b'foo\n# comment at end\n'), - (b'foo\nbar\n', [], FAIL, b'bar\nfoo\n'), - (b'bar\nfoo\n', [], PASS, b'bar\nfoo\n'), - (b'a\nc\nb\n', [], 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'#comment2\nbar\n#comment1\nfoo\n', - ), - ( - b'#comment1\nbar\n#comment2\nfoo\n', - [], - PASS, - b'#comment1\nbar\n#comment2\nfoo\n', - ), - (b'#comment\n\nfoo\nbar\n', [], FAIL, b'#comment\n\nbar\nfoo\n'), - (b'#comment\n\nbar\nfoo\n', [], PASS, b'#comment\n\nbar\nfoo\n'), - ( - b'foo\n\t#comment with indent\nbar\n', - [], - FAIL, - b'\t#comment with indent\nbar\nfoo\n', - ), - ( - b'bar\n\t#comment with indent\nfoo\n', - [], - PASS, - b'bar\n\t#comment with indent\nfoo\n', - ), - (b'\nfoo\nbar\n', [], FAIL, b'bar\n\nfoo\n'), - (b'\nbar\nfoo\n', [], PASS, b'\nbar\nfoo\n'), - ( - b'pyramid-foo==1\npyramid>=2\n', - [], - FAIL, - b'pyramid>=2\npyramid-foo==1\n', - ), - ( - b'a==1\n' - b'c>=1\n' - b'bbbb!=1\n' - b'c-a>=1;python_version>="3.6"\n' - b'e>=2\n' - b'd>2\n' - b'g<2\n' - b'f<=2\n', - [], - FAIL, - b'a==1\n' - b'bbbb!=1\n' - b'c>=1\n' - b'c-a>=1;python_version>="3.6"\n' - b'd>2\n' - b'e>=2\n' - b'f<=2\n' - b'g<2\n', - ), - (b'a==1\nb==1\na==1\n', [], FAIL, b'a==1\nb==1\n'), - ( - b'a==1\nb==1\n#comment about a\na==1\n', - [], - FAIL, - b'#comment about a\na==1\nb==1\n', - ), - (b'ocflib\nDjango\nPyMySQL\n', [], FAIL, b'Django\nocflib\nPyMySQL\n'), - ( - b'-e git+ssh://git_url@tag#egg=ocflib\nDjango\nPyMySQL\n', - [], - FAIL, - b'Django\n-e git+ssh://git_url@tag#egg=ocflib\nPyMySQL\n', - ), - (b'bar\npkg-resources==0.0.0\nfoo\n', [], FAIL, b'bar\nfoo\n'), - (b'foo\npkg-resources==0.0.0\nbar\n', [], FAIL, b'bar\nfoo\n'), - (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, - b'Django\nijk\ngit+ssh://git_url@tag#egg=ocflib\n', - ), - ( - b'b==1.0.0\n' - b'c=2.0.0 \\\n' - b' --hash=sha256:abcd\n' - b'a=3.0.0 \\\n' - b' --hash=sha256:a1b1c1d1', - [], - FAIL, - b'a=3.0.0 \\\n' - b' --hash=sha256:a1b1c1d1\n' - b'b==1.0.0\n' - b'c=2.0.0 \\\n' - b' --hash=sha256:abcd\n', - ), - ( - b'a=2.0.0 \\\n --hash=sha256:abcd\nb==1.0.0\n', - [], - PASS, - b'a=2.0.0 \\\n --hash=sha256:abcd\nb==1.0.0\n', - ), - (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'), + (b'', [], PASS, b''), + (b'\n', [], PASS, b'\n'), + (b'# intentionally empty\n', [], PASS, b'# intentionally empty\n'), + (b'foo\n# comment at end\n', [], PASS, b'foo\n# comment at end\n'), + (b'foo\nbar\n', [], FAIL, b'bar\nfoo\n'), + (b'bar\nfoo\n', [], PASS, b'bar\nfoo\n'), + (b'a\nc\nb\n', [], 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'#comment2\nbar\n#comment1\nfoo\n', + ), + ( + b'#comment1\nbar\n#comment2\nfoo\n', + [], + PASS, + b'#comment1\nbar\n#comment2\nfoo\n', + ), + (b'#comment\n\nfoo\nbar\n', [], FAIL, b'#comment\n\nbar\nfoo\n'), + (b'#comment\n\nbar\nfoo\n', [], PASS, b'#comment\n\nbar\nfoo\n'), + ( + b'foo\n\t#comment with indent\nbar\n', + [], + FAIL, + b'\t#comment with indent\nbar\nfoo\n', + ), + ( + b'bar\n\t#comment with indent\nfoo\n', + [], + PASS, + b'bar\n\t#comment with indent\nfoo\n', + ), + (b'\nfoo\nbar\n', [], FAIL, b'bar\n\nfoo\n'), + (b'\nbar\nfoo\n', [], PASS, b'\nbar\nfoo\n'), + ( + b'pyramid-foo==1\npyramid>=2\n', + [], + FAIL, + b'pyramid>=2\npyramid-foo==1\n', + ), + ( + b'a==1\n' + b'c>=1\n' + b'bbbb!=1\n' + b'c-a>=1;python_version>="3.6"\n' + b'e>=2\n' + b'd>2\n' + b'g<2\n' + b'f<=2\n', + [], + FAIL, + b'a==1\n' + b'bbbb!=1\n' + b'c>=1\n' + b'c-a>=1;python_version>="3.6"\n' + b'd>2\n' + b'e>=2\n' + b'f<=2\n' + b'g<2\n', + ), + (b'a==1\nb==1\na==1\n', [], FAIL, b'a==1\nb==1\n'), + ( + b'a==1\nb==1\n#comment about a\na==1\n', + [], + FAIL, + b'#comment about a\na==1\nb==1\n', + ), + (b'ocflib\nDjango\nPyMySQL\n', [], FAIL, b'Django\nocflib\nPyMySQL\n'), + ( + b'-e git+ssh://git_url@tag#egg=ocflib\nDjango\nPyMySQL\n', + [], + FAIL, + b'Django\n-e git+ssh://git_url@tag#egg=ocflib\nPyMySQL\n', + ), + (b'bar\npkg-resources==0.0.0\nfoo\n', [], FAIL, b'bar\nfoo\n'), + (b'foo\npkg-resources==0.0.0\nbar\n', [], FAIL, b'bar\nfoo\n'), + (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, + b'Django\nijk\ngit+ssh://git_url@tag#egg=ocflib\n', + ), + ( + b'b==1.0.0\n' + b'c=2.0.0 \\\n' + b' --hash=sha256:abcd\n' + b'a=3.0.0 \\\n' + b' --hash=sha256:a1b1c1d1', + [], + FAIL, + b'a=3.0.0 \\\n' + b' --hash=sha256:a1b1c1d1\n' + b'b==1.0.0\n' + b'c=2.0.0 \\\n' + b' --hash=sha256:abcd\n', + ), + ( + b'a=2.0.0 \\\n --hash=sha256:abcd\nb==1.0.0\n', + [], + PASS, + b'a=2.0.0 \\\n --hash=sha256:abcd\nb==1.0.0\n', + ), + (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, argv, expected_retval, output, tmpdir): - path = tmpdir.join('file.txt') - path.write_binary(input_s) + path = tmpdir.join('file.txt') + path.write_binary(input_s) - output_retval = main([str(path)] + argv) + output_retval = main([str(path)] + argv) - assert path.read_binary() == output - assert output_retval == expected_retval + assert path.read_binary() == output + assert output_retval == expected_retval def test_requirement_object(): - top_of_file = Requirement() - top_of_file.comments.append(b'#foo') - top_of_file.value = b'\n' + top_of_file = Requirement() + top_of_file.comments.append(b'#foo') + top_of_file.value = b'\n' - requirement_foo = Requirement() - requirement_foo.value = b'foo' + requirement_foo = Requirement() + requirement_foo.value = b'foo' - requirement_bar = Requirement() - requirement_bar.value = b'bar' + requirement_bar = Requirement() + requirement_bar.value = b'bar' - requirements_bar_versioned = Requirement() - requirements_bar_versioned.value = b'bar==1.0' + requirements_bar_versioned = Requirement() + requirements_bar_versioned.value = b'bar==1.0' - # check for version specification - assert top_of_file.contains_version_specifier() is False - 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 + # check for version specification + assert top_of_file.contains_version_specifier() is False + 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 + # 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 From cd47a0867eed41fd08f58bf3c7cbdde8eff04b63 Mon Sep 17 00:00:00 2001 From: Yannik Henke <3532075+binka-dev@users.noreply.github.com> Date: Wed, 5 Mar 2025 10:56:03 +0100 Subject: [PATCH 4/4] fixed formatting issues --- pre_commit_hooks/requirements_txt_fixer.py | 9 +++++++-- tests/requirements_txt_fixer_test.py | 21 ++++++++++++++++++--- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/pre_commit_hooks/requirements_txt_fixer.py b/pre_commit_hooks/requirements_txt_fixer.py index 8e97bcf1..04590e43 100644 --- a/pre_commit_hooks/requirements_txt_fixer.py +++ b/pre_commit_hooks/requirements_txt_fixer.py @@ -60,7 +60,10 @@ def is_complete(self) -> bool: ) def contains_version_specifier(self) -> bool: - return bool(self.VERSION_SPECIFIED.match(self.value)) + return ( + self.value is not None and + bool(self.VERSION_SPECIFIED.match(self.value)) + ) def append_value(self, value: bytes) -> None: if self.value is not None: @@ -171,7 +174,9 @@ def main(argv: Sequence[str] | None = None) -> int: for arg in args.filenames: with open(arg, 'rb+') as file_obj: - ret_for_file = fix_requirements(file_obj, args.fail_without_version) + ret_for_file = fix_requirements( + file_obj, args.fail_without_version, + ) if ret_for_file: print(f'Error in file {arg}') diff --git a/tests/requirements_txt_fixer_test.py b/tests/requirements_txt_fixer_test.py index b12f2f39..b35585ce 100644 --- a/tests/requirements_txt_fixer_test.py +++ b/tests/requirements_txt_fixer_test.py @@ -120,9 +120,24 @@ 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'), + ( + 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, argv, expected_retval, output, tmpdir):