diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index c6590ec..85acfc1 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -32,7 +32,8 @@ repos:
# Class cannot subclass "ConfigSchema" (has type "Any")
- --allow-subclassing-any
- repo: https://github.com/flakeheaven/flakeheaven
- rev: 0.11.1
+ rev: 3.2.1
hooks:
- name: Run flakeheaven static analysis tool
id: flakeheaven
+ files: ^(src|tests)/.+?\.(ipynb|md|py|rst|yaml|yml)$
diff --git a/docs/index.md b/docs/index.md
index ce2f5b6..15b0f93 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -200,6 +200,18 @@ export YAMLFIX_CONFIG_PATH="/etc/yamlfix/"
Configure the base config-path that `maison` will look for a `pyproject.toml` configuration file. This path is traversed upwards until such a file is found.
+### Document Fix ID
+
+Default: `document_fix_id: str = uuid.uuid4().hex`
+Environment variable override:
+```bash
+export YAMLFIX_DOCUMENT_FIX_ID="myid"
+```
+
+Warning: You should not modify this value, if you're not sure you need to.
+
+This generates a UUID in hex-string-representation (no delimiters), which is used internally to generate a temporary top-level mapping node, where any lists or comments can be attached, so ruyaml is not removing comments, and comment-only documents can be formatted properly.
+
### Explicit Document Start
Default: `explicit_start: bool = True`
diff --git a/src/yamlfix/adapters.py b/src/yamlfix/adapters.py
index 3b292e7..7db4529 100644
--- a/src/yamlfix/adapters.py
+++ b/src/yamlfix/adapters.py
@@ -224,6 +224,13 @@ def patch_sequence_style(key_node: Node, value_node: Node) -> None:
if not sequence_node.value:
return
+ # if this key_node is the `yamlfix_document_fix_id` node,
+ # the sequence_node is the top-level list, which has to be forced
+ # into block-mode, so the key_node can be removed afterwards
+ force_block_style = force_block_style or self._seq_is_in_top_level_node(
+ key_node
+ )
+
# if this sequence contains non-scalar nodes (i.e. dicts, lists, etc.),
# force block-style
force_block_style = (
@@ -252,6 +259,9 @@ def patch_sequence_style(key_node: Node, value_node: Node) -> None:
self.patch_functions.append(patch_sequence_style)
+ def _seq_is_in_top_level_node(self, key_node: Node) -> bool:
+ return str(key_node.value) == f"yamlfix_{self.config.document_fix_id}"
+
@staticmethod
def _seq_contains_non_scalar_nodes(seq_node: Node) -> bool:
return any(not isinstance(node, ScalarNode) for node in seq_node.value)
@@ -342,9 +352,11 @@ def fix(self, source_code: str) -> str:
log.debug("Running source code fixers...")
fixers = [
+ self._fix_comment_only_files,
self._fix_truthy_strings,
self._fix_jinja_variables,
self._ruamel_yaml_fixer,
+ self._restore_comment_only_files,
self._restore_truthy_strings,
self._restore_jinja_variables,
self._restore_double_exclamations,
@@ -693,3 +705,33 @@ def _restore_jinja_variables(source_code: str) -> str:
fixed_source_lines.append(line)
return "\n".join(fixed_source_lines)
+
+ def _fix_comment_only_files(self, source_code: str) -> str:
+ """Add a mapping key with an id to the start of the document\
+ to preserve comments."""
+ fixed_source_lines = []
+ yamlfix_document_id_line = f"yamlfix_{self.config.document_fix_id}:"
+
+ # Add the document id line after each document start
+ has_start_indicator = False
+ for line in source_code.splitlines():
+ fixed_source_lines.append(line)
+ if line.startswith("---"):
+ has_start_indicator = True
+ fixed_source_lines.append(yamlfix_document_id_line)
+
+ # if the document has no start indicator, the document id as the first line
+ if not has_start_indicator:
+ fixed_source_lines.insert(0, yamlfix_document_id_line)
+
+ return "\n".join(fixed_source_lines)
+
+ def _restore_comment_only_files(self, source_code: str) -> str:
+ """Remove the document start id from the document again."""
+ fixed_source_lines = []
+
+ for line in source_code.splitlines():
+ if self.config.document_fix_id not in line:
+ fixed_source_lines.append(line)
+
+ return "\n".join(fixed_source_lines)
diff --git a/src/yamlfix/model.py b/src/yamlfix/model.py
index e969cac..364fa3b 100644
--- a/src/yamlfix/model.py
+++ b/src/yamlfix/model.py
@@ -1,5 +1,6 @@
"""Define program entities like configuration value entities."""
+import uuid
from typing import Optional
from maison.schema import ConfigSchema
@@ -12,6 +13,7 @@ class YamlfixConfig(ConfigSchema):
comments_min_spaces_from_content: int = 2
comments_require_starting_space: bool = True
config_path: Optional[str] = None
+ document_fix_id: str = uuid.uuid4().hex
explicit_start: bool = True
flow_style_sequence: Optional[bool] = True
indent_mapping: int = 2
diff --git a/tests/unit/test_services.py b/tests/unit/test_services.py
index c5fa3fd..82eb089 100644
--- a/tests/unit/test_services.py
+++ b/tests/unit/test_services.py
@@ -184,6 +184,51 @@ def test_fix_code_preserves_comments(self) -> None:
assert result == source
+ def test_fix_code_preserves_comments_without_start_indication(self) -> None:
+ """Don't delete comments without yaml explicit start indictor."""
+ source = dedent(
+ """\
+ # Keep comments!
+ program: yamlfix
+ """
+ )
+ config = YamlfixConfig()
+ config.explicit_start = False
+
+ result = fix_code(source, config)
+
+ assert result == source
+
+ def test_fix_code_preserves_comment_only_file(self) -> None:
+ """Don't delete comments even if the file is only comments."""
+ source = dedent(
+ """\
+ ---
+ # Keep comments!
+ """
+ )
+
+ result = fix_code(source)
+
+ assert result == source
+
+ def test_fix_code_preserves_comment_only_files_without_start_indication(
+ self,
+ ) -> None:
+ """Don't delete comments even if the file is only comments, without\
+ start indication."""
+ source = dedent(
+ """\
+ # Keep comments!
+ """
+ )
+ config = YamlfixConfig()
+ config.explicit_start = False
+
+ result = fix_code(source, config)
+
+ assert result == source
+
def test_fix_code_respects_parent_lists_with_comments(self) -> None:
"""Do not indent lists at the first level even if there is a comment."""
source = dedent(