diff --git a/CHANGELOG.md b/CHANGELOG.md index 327b47f..ba1e3e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [unreleased] + +### Added +- settings: Support optional section-level documentation via `_doc` in settings + definition (#80). + ## [1.7.0] - 2026-05-08 ### Added diff --git a/src/settings/rfl/settings/definition.py b/src/settings/rfl/settings/definition.py index 1649bbf..34e1620 100644 --- a/src/settings/rfl/settings/definition.py +++ b/src/settings/rfl/settings/definition.py @@ -212,11 +212,19 @@ def __str__(self): class SettingsSectionDefinition: - def __init__(self, section: str, parameters: dict): + SECTION_DOC_KEY = "_doc" + + def __init__(self, section: str, content: dict): self.name = section + self.doc = content.get(self.SECTION_DOC_KEY) + if self.doc is not None and not isinstance(self.doc, str): + raise SettingsDefinitionError( + f"Invalid type for section {self.SECTION_DOC_KEY} in [{section}]" + ) self.parameters = [ SettingsParameterDefinition(section, parameter, properties) - for parameter, properties in parameters.items() + for parameter, properties in content.items() + if parameter != self.SECTION_DOC_KEY ] def has_parameter(self, name: str) -> bool: @@ -236,8 +244,8 @@ class SettingsDefinition: def __init__(self, loader: SettingsDefinitionLoader): self.loader = loader self.sections = [ - SettingsSectionDefinition(section, parameters) - for section, parameters in loader.content.items() + SettingsSectionDefinition(section, content) + for section, content in loader.content.items() ] self._validate_deprecated_references() diff --git a/src/settings/rfl/tests/test_settings.py b/src/settings/rfl/tests/test_settings.py index 7826bba..9772664 100644 --- a/src/settings/rfl/tests/test_settings.py +++ b/src/settings/rfl/tests/test_settings.py @@ -124,6 +124,48 @@ def test_valid_content(self): self.assertEqual( definition.section("section2").parameter("param_uri")._type, "uri" ) + self.assertIsNone(definition.section("section1").doc) + self.assertIsNone(definition.section("section2").doc) + + def test_section_doc_valid(self): + loader = SettingsDefinitionLoaderYaml(raw=VALID_DEFINITION) + loader.content["section2"]["_doc"] = "Documentation for section2" + definition = SettingsDefinition(loader) + self.assertEqual( + definition.section("section2").doc, "Documentation for section2" + ) + self.assertEqual( + definition.section("section2").parameter("param_int").default, 10 + ) + + def test_section_doc_invalid_type_int(self): + loader = SettingsDefinitionLoaderYaml(raw=VALID_DEFINITION) + loader.content["section2"]["_doc"] = 42 + with self.assertRaisesRegex( + SettingsDefinitionError, + r"^Invalid type for section _doc in \[section2\]$", + ): + SettingsDefinition(loader) + + def test_section_doc_invalid_type_dict(self): + loader = SettingsDefinitionLoaderYaml(raw=VALID_DEFINITION) + loader.content["section2"]["_doc"] = {"type": "str", "default": "fail"} + with self.assertRaisesRegex( + SettingsDefinitionError, + r"^Invalid type for section _doc in \[section2\]$", + ): + SettingsDefinition(loader) + + def test_section_doc_with_parameter_named_doc(self): + loader = SettingsDefinitionLoaderYaml(raw=VALID_DEFINITION) + loader.content["section2"]["_doc"] = "Section documentation" + loader.content["section2"]["doc"] = {"type": "str", "default": "value"} + definition = SettingsDefinition(loader) + self.assertEqual(definition.section("section2").doc, "Section documentation") + self.assertTrue(definition.section("section2").has_parameter("doc")) + self.assertEqual( + definition.section("section2").parameter("doc").default, "value" + ) def test_invalid_yaml(self): with self.assertRaisesRegex(