diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2022-01-30 11:02:58 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-21 20:41:55 +0000 |
commit | d4583dcad7d68d3c1503b04ec0d3364809304807 (patch) | |
tree | 82456c5d0bc77961759812ddd85414435ba89127 /pre_commit_hooks/sort_simple_yaml.py | |
parent | Initial commit. (diff) | |
download | pre-commit-hooks-d4583dcad7d68d3c1503b04ec0d3364809304807.tar.xz pre-commit-hooks-d4583dcad7d68d3c1503b04ec0d3364809304807.zip |
Adding upstream version 4.5.0+dfsg.upstream/4.5.0+dfsg
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'pre_commit_hooks/sort_simple_yaml.py')
-rw-r--r-- | pre_commit_hooks/sort_simple_yaml.py | 125 |
1 files changed, 125 insertions, 0 deletions
diff --git a/pre_commit_hooks/sort_simple_yaml.py b/pre_commit_hooks/sort_simple_yaml.py new file mode 100644 index 0000000..116b5c1 --- /dev/null +++ b/pre_commit_hooks/sort_simple_yaml.py @@ -0,0 +1,125 @@ +"""Sort a simple YAML file, keeping blocks of comments and definitions +together. + +We assume a strict subset of YAML that looks like: + + # block of header comments + # here that should always + # be at the top of the file + + # optional comments + # can go here + key: value + key: value + + key: value + +In other words, we don't sort deeper than the top layer, and might corrupt +complicated YAML files. +""" +from __future__ import annotations + +import argparse +from typing import Sequence + + +QUOTES = ["'", '"'] + + +def sort(lines: list[str]) -> list[str]: + """Sort a YAML file in alphabetical order, keeping blocks together. + + :param lines: array of strings (without newlines) + :return: sorted array of strings + """ + # make a copy of lines since we will clobber it + lines = list(lines) + new_lines = parse_block(lines, header=True) + + for block in sorted(parse_blocks(lines), key=first_key): + if new_lines: + new_lines.append('') + new_lines.extend(block) + + return new_lines + + +def parse_block(lines: 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 + comment. Otherwise, we stop after reaching an empty line. + + :param lines: list of lines + :param header: whether we are parsing a header block + :return: list of lines that form the single block + """ + block_lines = [] + while lines and lines[0] and (not header or lines[0].startswith('#')): + block_lines.append(lines.pop(0)) + return block_lines + + +def parse_blocks(lines: list[str]) -> list[list[str]]: + """Parse and return all possible blocks, popping off the start of `lines`. + + :param lines: list of lines + :return: list of blocks, where each block is a list of lines + """ + blocks = [] + + while lines: + if lines[0] == '': + lines.pop(0) + else: + blocks.append(parse_block(lines)) + + return blocks + + +def first_key(lines: 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 + stripping leading quotes. + + >>> print(test) + # some comment + 'foo': true + >>> first_key(test) + 'foo' + """ + for line in lines: + if line.startswith('#'): + continue + if any(line.startswith(quote) for quote in QUOTES): + return line[1:] + return line + else: + return '' # not actually reached in reality + + +def main(argv: Sequence[str] | None = None) -> int: + parser = argparse.ArgumentParser() + parser.add_argument('filenames', nargs='*', help='Filenames to fix') + args = parser.parse_args(argv) + + retval = 0 + + for filename in args.filenames: + with open(filename, 'r+') as f: + lines = [line.rstrip() for line in f.readlines()] + new_lines = sort(lines) + + if lines != new_lines: + print(f'Fixing file `{filename}`') + f.seek(0) + f.write('\n'.join(new_lines) + '\n') + f.truncate() + retval = 1 + + return retval + + +if __name__ == '__main__': + raise SystemExit(main()) |