From cfdad12279115cd6564f08210a0292eb04fb98b3 Mon Sep 17 00:00:00 2001 From: Lourens Veen Date: Sat, 23 May 2026 20:13:29 +0200 Subject: [PATCH 1/2] Strip trailing whitespace in descriptions to avoid messy formatting --- ymmsl/tests/test_io.py | 83 +++++++++++++++++++++ ymmsl/v0_2/component.py | 5 +- ymmsl/v0_2/configuration.py | 5 +- ymmsl/v0_2/implementation.py | 5 +- ymmsl/v0_2/supported_settings.py | 13 +++- ymmsl/v0_2/tests/test_supported_settings.py | 2 +- 6 files changed, 104 insertions(+), 9 deletions(-) diff --git a/ymmsl/tests/test_io.py b/ymmsl/tests/test_io.py index fae2aed..9c128f2 100644 --- a/ymmsl/tests/test_io.py +++ b/ymmsl/tests/test_io.py @@ -1,4 +1,5 @@ import ymmsl +from ymmsl import v0_2 import yatiml import pytest @@ -8,3 +9,85 @@ def test_invalid_version() -> None: """This is a regression test, the error was really confusing""" with pytest.raises(yatiml.RecognitionError): ymmsl.load('ymmsl_version: v0_1') + + +def test_component_description_trailing_whitespace() -> None: + """Regression test + + PyYAML refuses to use block mode if there is trailing whitespace. + """ + c = v0_2.Component('c1', v0_2.Ports(), 'Test \nmore test!') + dump = yatiml.dumps_function(v0_2.Component, v0_2.Ports, v0_2.Reference) + text = dump(c) + assert text == ( + 'name: c1\n' + 'ports: {}\n' + 'description: |\n' + ' Test\n' + ' more test!\n' + ) + + +def test_configuration_description_trailing_whitespace() -> None: + """Regression test, see above""" + c = v0_2.Configuration('Test\nmore test! ') + dump = yatiml.dumps_function(v0_2.Configuration, v0_2.Settings, v0_2.Checkpoints) + text = dump(c) + assert text == ( + 'description: |\n' + ' Test\n' + ' more test!\n' + ) + + +def test_model_description_trailing_whitespace() -> None: + """Regression test, see above""" + m = v0_2.Model('test_model', v0_2.Ports(), 'Test\nmore test \nand more') + dump = yatiml.dumps_function( + v0_2.Model, v0_2.Implementation, v0_2.Ports, v0_2.Reference) + text = dump(m) + assert text == ( + 'name: test_model\n' + 'description: |\n' + ' Test\n' + ' more test\n' + ' and more\n' + 'components: {}\n' + ) + + +def test_program_description_trailing_whitespace() -> None: + """Regression test, see above""" + p = v0_2.Program('test_program', v0_2.Ports(), 'Test ', script='') + dump = yatiml.dumps_function( + v0_2.Program, v0_2.BaseEnv, v0_2.ExecutionModel, v0_2.Implementation, + v0_2.KeepsStateForNextUse, v0_2.Ports, v0_2.Reference) + text = dump(p) + assert text == ( + 'name: test_program\n' + 'description: |\n' + ' Test\n' + 'script: \'\'\n' + ) + + +def test_supported_settings_trailing_whitespace() -> None: + """Regression test, see above""" + s = v0_2.SupportedSettings({ + 'alpha': 'float Collision angle ', + 'beta': 'float Dampening coefficient', + 'gamma': 'int Number\n of\nrays '}) + dump = yatiml.dumps_function( + v0_2.SupportedSettings, v0_2.Identifier, v0_2.SettingType, + v0_2.SupportedSetting) + text = dump(s) + assert text == ( + 'alpha: float Collision angle\n' + 'beta: float Dampening coefficient\n' + 'gamma:\n' + ' type: int\n' + ' description: |\n' + ' Number\n' + ' of\n' + ' rays\n' + ) diff --git a/ymmsl/v0_2/component.py b/ymmsl/v0_2/component.py index ac9a1dd..d110932 100644 --- a/ymmsl/v0_2/component.py +++ b/ymmsl/v0_2/component.py @@ -135,8 +135,9 @@ def _yatiml_sweeten(cls, node: yatiml.Node) -> None: # output in block style ynode = cast(yaml.ScalarNode, descr.yaml_node) ynode.style = '|' - if not ynode.value.endswith('\n'): - ynode.value += '\n' + # remove trailing whitespace to ensure PyYAML actually uses block style + ynode.value = '\n'.join([ + line.rstrip() for line in ynode.value.split('\n')]) + '\n' multiplicity = node.get_attribute('multiplicity') items = multiplicity.seq_items() diff --git a/ymmsl/v0_2/configuration.py b/ymmsl/v0_2/configuration.py index c8678fd..7efe72a 100644 --- a/ymmsl/v0_2/configuration.py +++ b/ymmsl/v0_2/configuration.py @@ -634,8 +634,9 @@ def _yatiml_sweeten(cls, node: yatiml.Node) -> None: # output in block style ynode = cast(yaml.ScalarNode, descr.yaml_node) ynode.style = '|' - if not ynode.value.endswith('\n'): - ynode.value += '\n' + # remove trailing whitespace to ensure PyYAML actually uses block style + ynode.value = '\n'.join([ + line.rstrip() for line in ynode.value.split('\n')]) + '\n' imports = node.get_attribute('imports') if imports.is_sequence() and imports.is_empty(): diff --git a/ymmsl/v0_2/implementation.py b/ymmsl/v0_2/implementation.py index 567438d..6623550 100644 --- a/ymmsl/v0_2/implementation.py +++ b/ymmsl/v0_2/implementation.py @@ -56,8 +56,9 @@ def _yatiml_sweeten(cls, node: yatiml.Node) -> None: # output in block style ynode = cast(yaml.ScalarNode, descr.yaml_node) ynode.style = '|' - if not ynode.value.endswith('\n'): - ynode.value += '\n' + # remove trailing whitespace to ensure PyYAML actually uses block style + ynode.value = '\n'.join([ + line.rstrip() for line in ynode.value.split('\n')]) + '\n' if len(node.get_attribute('supported_settings').yaml_node.value) == 0: node.remove_attribute('supported_settings') diff --git a/ymmsl/v0_2/supported_settings.py b/ymmsl/v0_2/supported_settings.py index cde9cda..9c8001f 100644 --- a/ymmsl/v0_2/supported_settings.py +++ b/ymmsl/v0_2/supported_settings.py @@ -256,8 +256,17 @@ def _yatiml_sweeten(cls, node: yatiml.Node) -> None: """If the description is multiple lines, format it block style""" desc_node = node.get_attribute('description') if desc_node.is_scalar(str): + ynode = cast(yaml.ScalarNode, desc_node.yaml_node) if '\n' in cast(str, desc_node.get_value()): - cast(yaml.ScalarNode, desc_node.yaml_node).style = '|' + ynode.style = '|' + + # remove trailing whitespace to ensure PyYAML actually uses block style + ynode.value = '\n'.join([ + line.rstrip() for line in ynode.value.split('\n')]) + '\n' + + else: + # we do it also for single-line descriptions for consistency + ynode.value = ynode.value.rstrip() class SupportedSettings(MutableMapping): @@ -279,7 +288,7 @@ class SupportedSettings(MutableMapping): - low: Not very accurate, but fast. Good for testing. - medium: Slower and more accurate. Good for most use. - - hight: Slowest, but very accurate. Good for reference runs. + - high: Slowest, but very accurate. Good for reference runs. D: '[[float]] Diffusion kernel' D2: [[float]] diff --git a/ymmsl/v0_2/tests/test_supported_settings.py b/ymmsl/v0_2/tests/test_supported_settings.py index 5190bb8..7340304 100644 --- a/ymmsl/v0_2/tests/test_supported_settings.py +++ b/ymmsl/v0_2/tests/test_supported_settings.py @@ -191,7 +191,7 @@ def test_dump_descriptions() -> None: 'd: str With single-line description\n' 'e:\n' ' type: [float]\n' - ' description: |-\n' + ' description: |\n' ' With multiline\n' ' description\n' ) From 627b3964913c5efe7475afea18d3cbd9d5025255 Mon Sep 17 00:00:00 2001 From: Lourens Veen Date: Sat, 30 May 2026 20:16:33 +0200 Subject: [PATCH 2/2] Make trailing whitespace utility function to avoid repetition --- ymmsl/util.py | 7 +++++++ ymmsl/v0_2/component.py | 6 +++--- ymmsl/v0_2/configuration.py | 6 +++--- ymmsl/v0_2/implementation.py | 6 +++--- ymmsl/v0_2/supported_settings.py | 6 +++--- 5 files changed, 19 insertions(+), 12 deletions(-) create mode 100644 ymmsl/util.py diff --git a/ymmsl/util.py b/ymmsl/util.py new file mode 100644 index 0000000..785aa67 --- /dev/null +++ b/ymmsl/util.py @@ -0,0 +1,7 @@ +def remove_trailing_whitespace(text: str) -> str: + """Remove trailing whitespace from each line in text + + This ensures that each line in text ends with only a newline, with no whitespace in + between the text and that newline. + """ + return '\n'.join([line.rstrip() for line in text.split('\n')]) + '\n' diff --git a/ymmsl/v0_2/component.py b/ymmsl/v0_2/component.py index d110932..2623f73 100644 --- a/ymmsl/v0_2/component.py +++ b/ymmsl/v0_2/component.py @@ -3,6 +3,7 @@ import yaml import yatiml +from ymmsl.util import remove_trailing_whitespace from ymmsl.v0_2.ports import Ports from ymmsl.v0_2.identity import Reference @@ -135,9 +136,8 @@ def _yatiml_sweeten(cls, node: yatiml.Node) -> None: # output in block style ynode = cast(yaml.ScalarNode, descr.yaml_node) ynode.style = '|' - # remove trailing whitespace to ensure PyYAML actually uses block style - ynode.value = '\n'.join([ - line.rstrip() for line in ynode.value.split('\n')]) + '\n' + # ensure PyYAML actually uses block style + ynode.value = remove_trailing_whitespace(ynode.value) multiplicity = node.get_attribute('multiplicity') items = multiplicity.seq_items() diff --git a/ymmsl/v0_2/configuration.py b/ymmsl/v0_2/configuration.py index 7efe72a..8ace49b 100644 --- a/ymmsl/v0_2/configuration.py +++ b/ymmsl/v0_2/configuration.py @@ -9,6 +9,7 @@ import yatiml import yaml +from ymmsl.util import remove_trailing_whitespace from ymmsl.v0_2.checkpoint import Checkpoints from ymmsl.v0_2.execution import ExecutionModel from ymmsl.v0_2.resources import ( @@ -634,9 +635,8 @@ def _yatiml_sweeten(cls, node: yatiml.Node) -> None: # output in block style ynode = cast(yaml.ScalarNode, descr.yaml_node) ynode.style = '|' - # remove trailing whitespace to ensure PyYAML actually uses block style - ynode.value = '\n'.join([ - line.rstrip() for line in ynode.value.split('\n')]) + '\n' + # ensure PyYAML actually uses block style + ynode.value = remove_trailing_whitespace(ynode.value) imports = node.get_attribute('imports') if imports.is_sequence() and imports.is_empty(): diff --git a/ymmsl/v0_2/implementation.py b/ymmsl/v0_2/implementation.py index 6623550..16b6b3c 100644 --- a/ymmsl/v0_2/implementation.py +++ b/ymmsl/v0_2/implementation.py @@ -3,6 +3,7 @@ import yaml import yatiml +from ymmsl.util import remove_trailing_whitespace from ymmsl.v0_2.identity import Reference from ymmsl.v0_2.ports import Ports from ymmsl.v0_2.supported_settings import SupportedSettings @@ -56,9 +57,8 @@ def _yatiml_sweeten(cls, node: yatiml.Node) -> None: # output in block style ynode = cast(yaml.ScalarNode, descr.yaml_node) ynode.style = '|' - # remove trailing whitespace to ensure PyYAML actually uses block style - ynode.value = '\n'.join([ - line.rstrip() for line in ynode.value.split('\n')]) + '\n' + # ensure PyYAML actually uses block style + ynode.value = remove_trailing_whitespace(ynode.value) if len(node.get_attribute('supported_settings').yaml_node.value) == 0: node.remove_attribute('supported_settings') diff --git a/ymmsl/v0_2/supported_settings.py b/ymmsl/v0_2/supported_settings.py index 9c8001f..721abd3 100644 --- a/ymmsl/v0_2/supported_settings.py +++ b/ymmsl/v0_2/supported_settings.py @@ -5,6 +5,7 @@ import yaml import yatiml +from ymmsl.util import remove_trailing_whitespace from ymmsl.v0_2.identity import Identifier @@ -260,9 +261,8 @@ def _yatiml_sweeten(cls, node: yatiml.Node) -> None: if '\n' in cast(str, desc_node.get_value()): ynode.style = '|' - # remove trailing whitespace to ensure PyYAML actually uses block style - ynode.value = '\n'.join([ - line.rstrip() for line in ynode.value.split('\n')]) + '\n' + # ensure PyYAML actually uses block style + ynode.value = remove_trailing_whitespace(ynode.value) else: # we do it also for single-line descriptions for consistency