From 2923a3e88bcaf4e14b7e63d2166f28889e710bbe Mon Sep 17 00:00:00 2001 From: Clemens Schwaighofer Date: Tue, 6 Jan 2026 09:58:21 +0900 Subject: [PATCH] Fix settings loader to return empty list when splitting empty string value --- .../config_handling/settings_loader.py | 11 +- test-run/config_handling/config/settings.ini | 1 + test-run/config_handling/settings_loader.py | 13 +- .../config_handling/test_settings_loader.py | 119 ++++++++++-------- 4 files changed, 82 insertions(+), 62 deletions(-) diff --git a/src/corelibs/config_handling/settings_loader.py b/src/corelibs/config_handling/settings_loader.py index 24e091b..b4bd8ee 100644 --- a/src/corelibs/config_handling/settings_loader.py +++ b/src/corelibs/config_handling/settings_loader.py @@ -173,10 +173,13 @@ class SettingsLoader: args_overrride.append(key) if skip: continue - settings[config_id][key] = [ - __value.replace(" ", "") - for __value in settings[config_id][key].split(split_char) - ] + if settings[config_id][key]: + settings[config_id][key] = [ + __value.replace(" ", "") + for __value in settings[config_id][key].split(split_char) + ] + else: + settings[config_id][key] = [] except KeyError as e: raise ValueError(self.__print( f"[!] Cannot read [{config_id}] block because the entry [{e}] could not be found", diff --git a/test-run/config_handling/config/settings.ini b/test-run/config_handling/config/settings.ini index 1beb51f..8897fc7 100644 --- a/test-run/config_handling/config/settings.ini +++ b/test-run/config_handling/config/settings.ini @@ -12,6 +12,7 @@ some_match_list=foo,bar test_list=a,b,c,d f, g h other_list=a|b|c|d| third_list=xy|ab|df|fg +empty_list= str_length=foobar int_range=20 int_range_not_set= diff --git a/test-run/config_handling/settings_loader.py b/test-run/config_handling/settings_loader.py index 9ae80e8..ae4e15c 100644 --- a/test-run/config_handling/settings_loader.py +++ b/test-run/config_handling/settings_loader.py @@ -21,11 +21,6 @@ def main(): Main run """ - value = "2025/1/1" - regex_c = re.compile(SettingsLoaderCheck.CHECK_SETTINGS['string.date']['regex'], re.VERBOSE) - result = regex_c.search(value) - print(f"regex {regex_c} check against {value} -> {result}") - # for log testing log = Log( log_path=ROOT_PATH.joinpath(LOG_DIR, 'settings_loader.log'), @@ -37,6 +32,11 @@ def main(): ) log.logger.info('Settings loader') + value = "2025/1/1" + regex_c = re.compile(SettingsLoaderCheck.CHECK_SETTINGS['string.date']['regex'], re.VERBOSE) + result = regex_c.search(value) + log.info(f"regex {regex_c} check against {value} -> {result}") + sl = SettingsLoader( { 'overload_from_args': 'OVERLOAD from ARGS', @@ -69,6 +69,9 @@ def main(): "split:|", "check:string.alphanumeric" ], + "empty_list": [ + "split:,", + ], "str_length": [ "length:2-10" ], diff --git a/tests/unit/config_handling/test_settings_loader.py b/tests/unit/config_handling/test_settings_loader.py index bc3fcd6..deab012 100644 --- a/tests/unit/config_handling/test_settings_loader.py +++ b/tests/unit/config_handling/test_settings_loader.py @@ -16,7 +16,7 @@ class TestSettingsLoaderInit: def test_init_with_valid_config_file(self, tmp_path: Path): """Test initialization with a valid config file""" - config_file = tmp_path / "test.ini" + config_file = tmp_path.joinpath("test.ini") config_file.write_text("[Section]\nkey=value\n") loader = SettingsLoader( @@ -35,7 +35,7 @@ class TestSettingsLoaderInit: def test_init_with_missing_config_file(self, tmp_path: Path): """Test initialization with missing config file""" - config_file = tmp_path / "missing.ini" + config_file = tmp_path.joinpath("missing.ini") loader = SettingsLoader( args={}, @@ -60,7 +60,7 @@ class TestSettingsLoaderInit: def test_init_with_log(self, tmp_path: Path): """Test initialization with Log object""" - config_file = tmp_path / "test.ini" + config_file = tmp_path.joinpath("test.ini") config_file.write_text("[Section]\nkey=value\n") mock_log = Mock(spec=Log) @@ -80,7 +80,7 @@ class TestLoadSettings: def test_load_settings_basic(self, tmp_path: Path): """Test loading basic settings without validation""" - config_file = tmp_path / "test.ini" + config_file = tmp_path.joinpath("test.ini") config_file.write_text("[TestSection]\nkey1=value1\nkey2=value2\n") loader = SettingsLoader(args={}, config_file=config_file) @@ -90,7 +90,7 @@ class TestLoadSettings: def test_load_settings_with_missing_section(self, tmp_path: Path): """Test loading settings with missing section""" - config_file = tmp_path / "test.ini" + config_file = tmp_path.joinpath("test.ini") config_file.write_text("[OtherSection]\nkey=value\n") loader = SettingsLoader(args={}, config_file=config_file) @@ -100,7 +100,7 @@ class TestLoadSettings: def test_load_settings_allow_not_exist(self, tmp_path: Path): """Test loading settings with allow_not_exist flag""" - config_file = tmp_path / "test.ini" + config_file = tmp_path.joinpath("test.ini") config_file.write_text("[OtherSection]\nkey=value\n") loader = SettingsLoader(args={}, config_file=config_file) @@ -110,7 +110,7 @@ class TestLoadSettings: def test_load_settings_mandatory_field_present(self, tmp_path: Path): """Test mandatory field validation when field is present""" - config_file = tmp_path / "test.ini" + config_file = tmp_path.joinpath("test.ini") config_file.write_text("[TestSection]\nrequired_field=value\n") loader = SettingsLoader(args={}, config_file=config_file) @@ -123,7 +123,7 @@ class TestLoadSettings: def test_load_settings_mandatory_field_missing(self, tmp_path: Path): """Test mandatory field validation when field is missing""" - config_file = tmp_path / "test.ini" + config_file = tmp_path.joinpath("test.ini") config_file.write_text("[TestSection]\nother_field=value\n") loader = SettingsLoader(args={}, config_file=config_file) @@ -136,7 +136,7 @@ class TestLoadSettings: def test_load_settings_mandatory_field_empty(self, tmp_path: Path): """Test mandatory field validation when field is empty""" - config_file = tmp_path / "test.ini" + config_file = tmp_path.joinpath("test.ini") config_file.write_text("[TestSection]\nrequired_field=\n") loader = SettingsLoader(args={}, config_file=config_file) @@ -149,7 +149,7 @@ class TestLoadSettings: def test_load_settings_with_split(self, tmp_path: Path): """Test splitting values into lists""" - config_file = tmp_path / "test.ini" + config_file = tmp_path.joinpath("test.ini") config_file.write_text("[TestSection]\nlist_field=a,b,c,d\n") loader = SettingsLoader(args={}, config_file=config_file) @@ -162,7 +162,7 @@ class TestLoadSettings: def test_load_settings_with_custom_split_char(self, tmp_path: Path): """Test splitting with custom delimiter""" - config_file = tmp_path / "test.ini" + config_file = tmp_path.joinpath("test.ini") config_file.write_text("[TestSection]\nlist_field=a|b|c|d\n") loader = SettingsLoader(args={}, config_file=config_file) @@ -175,7 +175,7 @@ class TestLoadSettings: def test_load_settings_split_removes_spaces(self, tmp_path: Path): """Test that split removes spaces from values""" - config_file = tmp_path / "test.ini" + config_file = tmp_path.joinpath("test.ini") config_file.write_text("[TestSection]\nlist_field=a, b , c , d\n") loader = SettingsLoader(args={}, config_file=config_file) @@ -188,7 +188,7 @@ class TestLoadSettings: def test_load_settings_empty_split_char_fallback(self, tmp_path: Path, capsys: CaptureFixture[str]): """Test fallback to default split char when empty""" - config_file = tmp_path / "test.ini" + config_file = tmp_path.joinpath("test.ini") config_file.write_text("[TestSection]\nlist_field=a,b,c\n") loader = SettingsLoader(args={}, config_file=config_file) @@ -201,9 +201,22 @@ class TestLoadSettings: captured = capsys.readouterr() assert "fallback to:" in captured.out + def test_load_settings_split_empty_value(self, tmp_path: Path): + """Test that split on empty value results in empty list""" + config_file = tmp_path.joinpath("test.ini") + config_file.write_text("[TestSection]\nlist_field=\n") + + loader = SettingsLoader(args={}, config_file=config_file) + result = loader.load_settings( + "TestSection", + {"list_field": ["split:,"]} + ) + + assert result["list_field"] == [] + def test_load_settings_convert_to_int(self, tmp_path: Path): """Test converting values to int""" - config_file = tmp_path / "test.ini" + config_file = tmp_path.joinpath("test.ini") config_file.write_text("[TestSection]\nnumber=123\n") loader = SettingsLoader(args={}, config_file=config_file) @@ -217,7 +230,7 @@ class TestLoadSettings: def test_load_settings_convert_to_float(self, tmp_path: Path): """Test converting values to float""" - config_file = tmp_path / "test.ini" + config_file = tmp_path.joinpath("test.ini") config_file.write_text("[TestSection]\nnumber=123.45\n") loader = SettingsLoader(args={}, config_file=config_file) @@ -231,7 +244,7 @@ class TestLoadSettings: def test_load_settings_convert_to_bool_true(self, tmp_path: Path): """Test converting values to boolean True""" - config_file = tmp_path / "test.ini" + config_file = tmp_path.joinpath("test.ini") config_file.write_text("[TestSection]\nflag1=true\nflag2=True\n") loader = SettingsLoader(args={}, config_file=config_file) @@ -245,7 +258,7 @@ class TestLoadSettings: def test_load_settings_convert_to_bool_false(self, tmp_path: Path): """Test converting values to boolean False""" - config_file = tmp_path / "test.ini" + config_file = tmp_path.joinpath("test.ini") config_file.write_text("[TestSection]\nflag1=false\nflag2=False\n") loader = SettingsLoader(args={}, config_file=config_file) @@ -259,7 +272,7 @@ class TestLoadSettings: def test_load_settings_convert_invalid_type(self, tmp_path: Path): """Test converting with invalid type raises error""" - config_file = tmp_path / "test.ini" + config_file = tmp_path.joinpath("test.ini") config_file.write_text("[TestSection]\nvalue=test\n") loader = SettingsLoader(args={}, config_file=config_file) @@ -272,7 +285,7 @@ class TestLoadSettings: def test_load_settings_empty_set_to_none(self, tmp_path: Path): """Test setting empty values to None""" - config_file = tmp_path / "test.ini" + config_file = tmp_path.joinpath("test.ini") config_file.write_text("[TestSection]\nother=value\n") loader = SettingsLoader(args={}, config_file=config_file) @@ -285,7 +298,7 @@ class TestLoadSettings: def test_load_settings_empty_set_to_custom_value(self, tmp_path: Path): """Test setting empty values to custom value""" - config_file = tmp_path / "test.ini" + config_file = tmp_path.joinpath("test.ini") config_file.write_text("[TestSection]\nother=value\n") loader = SettingsLoader(args={}, config_file=config_file) @@ -298,7 +311,7 @@ class TestLoadSettings: def test_load_settings_matching_valid(self, tmp_path: Path): """Test matching validation with valid value""" - config_file = tmp_path / "test.ini" + config_file = tmp_path.joinpath("test.ini") config_file.write_text("[TestSection]\nmode=production\n") loader = SettingsLoader(args={}, config_file=config_file) @@ -311,7 +324,7 @@ class TestLoadSettings: def test_load_settings_matching_invalid(self, tmp_path: Path): """Test matching validation with invalid value""" - config_file = tmp_path / "test.ini" + config_file = tmp_path.joinpath("test.ini") config_file.write_text("[TestSection]\nmode=invalid\n") loader = SettingsLoader(args={}, config_file=config_file) @@ -324,7 +337,7 @@ class TestLoadSettings: def test_load_settings_in_valid(self, tmp_path: Path): """Test 'in' validation with valid value""" - config_file = tmp_path / "test.ini" + config_file = tmp_path.joinpath("test.ini") config_file.write_text("[TestSection]\nallowed=a,b,c\nvalue=b\n") loader = SettingsLoader(args={}, config_file=config_file) @@ -340,7 +353,7 @@ class TestLoadSettings: def test_load_settings_in_invalid(self, tmp_path: Path): """Test 'in' validation with invalid value""" - config_file = tmp_path / "test.ini" + config_file = tmp_path.joinpath("test.ini") config_file.write_text("[TestSection]\nallowed=a,b,c\nvalue=d\n") loader = SettingsLoader(args={}, config_file=config_file) @@ -356,7 +369,7 @@ class TestLoadSettings: def test_load_settings_in_missing_target(self, tmp_path: Path): """Test 'in' validation with missing target""" - config_file = tmp_path / "test.ini" + config_file = tmp_path.joinpath("test.ini") config_file.write_text("[TestSection]\nvalue=a\n") loader = SettingsLoader(args={}, config_file=config_file) @@ -369,7 +382,7 @@ class TestLoadSettings: def test_load_settings_length_exact(self, tmp_path: Path): """Test length validation with exact match""" - config_file = tmp_path / "test.ini" + config_file = tmp_path.joinpath("test.ini") config_file.write_text("[TestSection]\nvalue=test\n") loader = SettingsLoader(args={}, config_file=config_file) @@ -382,7 +395,7 @@ class TestLoadSettings: def test_load_settings_length_exact_invalid(self, tmp_path: Path): """Test length validation with exact match failure""" - config_file = tmp_path / "test.ini" + config_file = tmp_path.joinpath("test.ini") config_file.write_text("[TestSection]\nvalue=test\n") loader = SettingsLoader(args={}, config_file=config_file) @@ -395,7 +408,7 @@ class TestLoadSettings: def test_load_settings_length_range(self, tmp_path: Path): """Test length validation with range""" - config_file = tmp_path / "test.ini" + config_file = tmp_path.joinpath("test.ini") config_file.write_text("[TestSection]\nvalue=testing\n") loader = SettingsLoader(args={}, config_file=config_file) @@ -408,7 +421,7 @@ class TestLoadSettings: def test_load_settings_length_min_only(self, tmp_path: Path): """Test length validation with minimum only""" - config_file = tmp_path / "test.ini" + config_file = tmp_path.joinpath("test.ini") config_file.write_text("[TestSection]\nvalue=testing\n") loader = SettingsLoader(args={}, config_file=config_file) @@ -421,7 +434,7 @@ class TestLoadSettings: def test_load_settings_length_max_only(self, tmp_path: Path): """Test length validation with maximum only""" - config_file = tmp_path / "test.ini" + config_file = tmp_path.joinpath("test.ini") config_file.write_text("[TestSection]\nvalue=test\n") loader = SettingsLoader(args={}, config_file=config_file) @@ -434,7 +447,7 @@ class TestLoadSettings: def test_load_settings_range_valid(self, tmp_path: Path): """Test range validation with valid value""" - config_file = tmp_path / "test.ini" + config_file = tmp_path.joinpath("test.ini") config_file.write_text("[TestSection]\nnumber=25\n") loader = SettingsLoader(args={}, config_file=config_file) @@ -447,7 +460,7 @@ class TestLoadSettings: def test_load_settings_range_invalid(self, tmp_path: Path): """Test range validation with invalid value""" - config_file = tmp_path / "test.ini" + config_file = tmp_path.joinpath("test.ini") config_file.write_text("[TestSection]\nnumber=100\n") loader = SettingsLoader(args={}, config_file=config_file) @@ -460,7 +473,7 @@ class TestLoadSettings: def test_load_settings_check_int_valid(self, tmp_path: Path): """Test check:int with valid integer""" - config_file = tmp_path / "test.ini" + config_file = tmp_path.joinpath("test.ini") config_file.write_text("[TestSection]\nnumber=12345\n") loader = SettingsLoader(args={}, config_file=config_file) @@ -473,7 +486,7 @@ class TestLoadSettings: def test_load_settings_check_int_cleanup(self, tmp_path: Path): """Test check:int with cleanup""" - config_file = tmp_path / "test.ini" + config_file = tmp_path.joinpath("test.ini") config_file.write_text("[TestSection]\nnumber=12a34b5\n") loader = SettingsLoader(args={}, config_file=config_file) @@ -486,7 +499,7 @@ class TestLoadSettings: def test_load_settings_check_email_valid(self, tmp_path: Path): """Test check:string.email.basic with valid email""" - config_file = tmp_path / "test.ini" + config_file = tmp_path.joinpath("test.ini") config_file.write_text("[TestSection]\nemail=test@example.com\n") loader = SettingsLoader(args={}, config_file=config_file) @@ -499,7 +512,7 @@ class TestLoadSettings: def test_load_settings_check_email_invalid(self, tmp_path: Path): """Test check:string.email.basic with invalid email""" - config_file = tmp_path / "test.ini" + config_file = tmp_path.joinpath("test.ini") config_file.write_text("[TestSection]\nemail=not-an-email\n") loader = SettingsLoader(args={}, config_file=config_file) @@ -512,7 +525,7 @@ class TestLoadSettings: def test_load_settings_args_override(self, tmp_path: Path, capsys: CaptureFixture[str]): """Test command line arguments override config values""" - config_file = tmp_path / "test.ini" + config_file = tmp_path.joinpath("test.ini") config_file.write_text("[TestSection]\nvalue=config_value\n") loader = SettingsLoader( @@ -530,7 +543,7 @@ class TestLoadSettings: def test_load_settings_args_no_flag(self, tmp_path: Path, capsys: CaptureFixture[str]): """Test default behavior (no args_override:yes) with list argument that has split""" - config_file = tmp_path / "test.ini" + config_file = tmp_path.joinpath("test.ini") config_file.write_text("[TestSection]\nvalue=a,b,c\n") loader = SettingsLoader( @@ -550,7 +563,7 @@ class TestLoadSettings: def test_load_settings_args_list_no_split(self, tmp_path: Path, capsys: CaptureFixture[str]): """Test that list arguments without split entry are skipped""" - config_file = tmp_path / "test.ini" + config_file = tmp_path.joinpath("test.ini") config_file.write_text("[TestSection]\nvalue=config_value\n") loader = SettingsLoader( @@ -570,7 +583,7 @@ class TestLoadSettings: def test_load_settings_args_list_with_split(self, tmp_path: Path, capsys: CaptureFixture[str]): """Test that list arguments with split entry and args_override:yes are applied""" - config_file = tmp_path / "test.ini" + config_file = tmp_path.joinpath("test.ini") config_file.write_text("[TestSection]\nvalue=a,b,c\n") loader = SettingsLoader( @@ -589,7 +602,7 @@ class TestLoadSettings: def test_load_settings_args_no_with_mandatory(self, tmp_path: Path, capsys: CaptureFixture[str]): """Test default behavior (no args_override:yes) with mandatory field and list args with split""" - config_file = tmp_path / "test.ini" + config_file = tmp_path.joinpath("test.ini") config_file.write_text("[TestSection]\nvalue=config1,config2\n") loader = SettingsLoader( @@ -609,7 +622,7 @@ class TestLoadSettings: def test_load_settings_args_no_with_mandatory_valid(self, tmp_path: Path, capsys: CaptureFixture[str]): """Test default behavior with string args (always overrides due to current logic)""" - config_file = tmp_path / "test.ini" + config_file = tmp_path.joinpath("test.ini") config_file.write_text("[TestSection]\nvalue=config_value\n") loader = SettingsLoader( @@ -628,7 +641,7 @@ class TestLoadSettings: def test_load_settings_args_string_no_split(self, tmp_path: Path, capsys: CaptureFixture[str]): """Test that string arguments with args_override:yes work normally""" - config_file = tmp_path / "test.ini" + config_file = tmp_path.joinpath("test.ini") config_file.write_text("[TestSection]\nvalue=config_value\n") loader = SettingsLoader( @@ -647,7 +660,7 @@ class TestLoadSettings: def test_load_settings_no_config_file_with_args(self, tmp_path: Path): """Test loading settings without config file but with mandatory args""" - config_file = tmp_path / "missing.ini" + config_file = tmp_path.joinpath("missing.ini") loader = SettingsLoader( args={"required": "value"}, @@ -662,7 +675,7 @@ class TestLoadSettings: def test_load_settings_no_config_file_missing_args(self, tmp_path: Path): """Test loading settings without config file and missing args""" - config_file = tmp_path / "missing.ini" + config_file = tmp_path.joinpath("missing.ini") loader = SettingsLoader(args={}, config_file=config_file) @@ -674,7 +687,7 @@ class TestLoadSettings: def test_load_settings_check_list_with_split(self, tmp_path: Path): """Test check validation with list values""" - config_file = tmp_path / "test.ini" + config_file = tmp_path.joinpath("test.ini") config_file.write_text("[TestSection]\nlist=abc,def,ghi\n") loader = SettingsLoader(args={}, config_file=config_file) @@ -687,7 +700,7 @@ class TestLoadSettings: def test_load_settings_check_list_cleanup(self, tmp_path: Path): """Test check validation cleans up list values""" - config_file = tmp_path / "test.ini" + config_file = tmp_path.joinpath("test.ini") config_file.write_text("[TestSection]\nlist=ab-c,de_f,gh!i\n") loader = SettingsLoader(args={}, config_file=config_file) @@ -700,7 +713,7 @@ class TestLoadSettings: def test_load_settings_invalid_check_type(self, tmp_path: Path): """Test with invalid check type""" - config_file = tmp_path / "test.ini" + config_file = tmp_path.joinpath("test.ini") config_file.write_text("[TestSection]\nvalue=test\n") loader = SettingsLoader(args={}, config_file=config_file) @@ -717,7 +730,7 @@ class TestComplexScenarios: def test_complex_validation_scenario(self, tmp_path: Path): """Test complex scenario with multiple validations""" - config_file = tmp_path / "test.ini" + config_file = tmp_path.joinpath("test.ini") config_file.write_text( "[Production]\n" "environment=production\n" @@ -758,7 +771,7 @@ class TestComplexScenarios: def test_email_list_validation(self, tmp_path: Path): """Test email list with validation""" - config_file = tmp_path / "test.ini" + config_file = tmp_path.joinpath("test.ini") config_file.write_text( "[EmailConfig]\n" "emails=test@example.com,admin@domain.org,user+tag@site.co.uk\n" @@ -775,7 +788,7 @@ class TestComplexScenarios: def test_mixed_args_and_config(self, tmp_path: Path): """Test mixing command line args and config file""" - config_file = tmp_path / "test.ini" + config_file = tmp_path.joinpath("test.ini") config_file.write_text( "[Settings]\n" "value1=config_value1\n" @@ -796,7 +809,7 @@ class TestComplexScenarios: def test_multiple_check_types(self, tmp_path: Path): """Test multiple different check types""" - config_file = tmp_path / "test.ini" + config_file = tmp_path.joinpath("test.ini") config_file.write_text( "[Checks]\n" "numbers=123,456,789\n" @@ -823,7 +836,7 @@ class TestComplexScenarios: def test_args_no_and_list_skip_combination(self, tmp_path: Path, capsys: CaptureFixture[str]): """Test combination of args_override:yes flag and list argument skip behavior""" - config_file = tmp_path / "test.ini" + config_file = tmp_path.joinpath("test.ini") config_file.write_text( "[Settings]\n" "no_override=a,b,c\n"