diff --git a/ci/sort-after.txt b/ci/sort-after.txt new file mode 100644 index 0000000..9aa6048 --- /dev/null +++ b/ci/sort-after.txt @@ -0,0 +1,10 @@ +--- +# Leave comment in place +foo: bar +quu: abcd # with comment +--- +bar: + baz: + # the following list should keep its order + - phfft + - blah diff --git a/ci/sort-before.txt b/ci/sort-before.txt new file mode 100644 index 0000000..b4a0cb4 --- /dev/null +++ b/ci/sort-before.txt @@ -0,0 +1,10 @@ +--- +# Leave comment in place +quu: abcd # with comment +foo: bar +--- +bar: + baz: + # the following list should keep its order + - phfft + - blah diff --git a/ci/test b/ci/test index 05f2116..6b99557 100755 --- a/ci/test +++ b/ci/test @@ -51,4 +51,12 @@ run pre_commit_hooks/yamlfmt --width 79 "${TMP_FILE}" run grep -v -q '.\{82\}' "${TMP_FILE}" run rm -f "${TMP_FILE}" +# Test sorting keys +TMP_FILE="$(mktemp pre-commit-hook-yamlfmt-test.XXXXXXXXXXXXXXXXXXXX)" +run cp -f ci/sort-before.txt "${TMP_FILE}" +run pre_commit_hooks/yamlfmt --mapping 4 --sequence 6 --offset 4 --sort_keys "${TMP_FILE}" +run diff ci/sort-after.txt "${TMP_FILE}" +# If the diff fails, the temp file is still there to inspect. +run rm -f "${TMP_FILE}" + run pre-commit run --all-files --hook-stage manual diff --git a/pre_commit_hooks/yamlfmt b/pre_commit_hooks/yamlfmt index bc565bf..721c1db 100755 --- a/pre_commit_hooks/yamlfmt +++ b/pre_commit_hooks/yamlfmt @@ -4,6 +4,7 @@ import argparse import sys from ruamel.yaml import YAML # pylint: disable=import-error +from ruamel.yaml.comments import CommentedMap # pylint: disable=import-error DEFAULT_INDENT = { "mapping": 4, @@ -103,6 +104,12 @@ class Cli: action="store_true", help="whether to keep null values" ) + parser.add_argument( + "-k", + "--sort_keys", + action="store_true", + help="whether to sort yaml keys" + ) parser.add_argument( "file_names", metavar="FILE_NAME", @@ -138,6 +145,7 @@ class Formatter: self.yaml = yaml self.path = kwargs.get("path", None) + self.sort_keys = kwargs.get("sort_keys", False) self.content = list({}) def format(self, path=None): @@ -165,7 +173,11 @@ class Formatter: path = self.path try: with open(path, "w", encoding='utf-8') as stream: - self.yaml.dump_all(self.content, stream) + if self.sort_keys: + _content = self.sort(self.content) + else: + _content = self.content + self.yaml.dump_all(_content, stream) except IOError: self.fail(f"Unable to write {path}") @@ -175,6 +187,21 @@ class Formatter: sys.stderr.write(msg) sys.exit(1) + @classmethod + def sort(cls, this): + """ Sort a content dictionary and keep comments """ + if isinstance(this, dict): + res = CommentedMap() + comment = this.ca + for k in sorted(this.keys()): + res[k] = cls.sort(this[k]) + res._yaml_comment = comment # pylint: disable=protected-access + return res + if isinstance(this, list): + for idx, elem in enumerate(this): + this[idx] = cls.sort(elem) + return this + if __name__ == "__main__": ARGS = Cli().parser.parse_args() @@ -187,7 +214,8 @@ if __name__ == "__main__": preserve_quotes=ARGS.preserve_quotes, preserve_null=ARGS.preserve_null, explicit_start=ARGS.explicit_start, - explicit_end=ARGS.explicit_end + explicit_end=ARGS.explicit_end, + sort_keys=ARGS.sort_keys ) for file_name in ARGS.file_names: FORMATTER.format(file_name)