diff --git a/changelog/33669.added.md b/changelog/33669.added.md new file mode 100644 index 00000000000..45fe6ead2ba --- /dev/null +++ b/changelog/33669.added.md @@ -0,0 +1,3 @@ +Issue #33669: Fixes an issue with the ``ini_managed`` execution module +where it would always wrap the separator with spaces. Adds a new parameter +named ``no_spaces`` that will not warp the separator with spaces. diff --git a/salt/modules/ini_manage.py b/salt/modules/ini_manage.py index 0cc62afb6fe..d2b6d7656d4 100644 --- a/salt/modules/ini_manage.py +++ b/salt/modules/ini_manage.py @@ -37,7 +37,7 @@ COM_REGX = re.compile(r"^\s*(#|;)\s*(.*)") INDENTED_REGX = re.compile(r"(\s+)(.*)") -def set_option(file_name, sections=None, separator="=", encoding=None): +def set_option(file_name, sections=None, separator="=", encoding=None, no_spaces=False): """ Edit an ini file, replacing one or more sections. Returns a dictionary containing the changes made. @@ -66,6 +66,14 @@ def set_option(file_name, sections=None, separator="=", encoding=None): .. versionadded:: 3006.6 + no_spaces (bool): + A bool value that specifies if the separator will be wrapped with + spaces. This parameter was added to have the ability to not wrap the + separator with spaces. Default is ``False``, which maintains + backwards compatibility. + + .. versionadded:: 3006.10 + Returns: dict: A dictionary representing the changes made to the ini file @@ -88,7 +96,9 @@ def set_option(file_name, sections=None, separator="=", encoding=None): """ sections = sections or {} - inifile = _Ini.get_ini_file(file_name, separator=separator, encoding=encoding) + inifile = _Ini.get_ini_file( + file_name, separator=separator, encoding=encoding, no_spaces=no_spaces + ) changes = inifile.update(sections) inifile.flush() return changes @@ -388,20 +398,19 @@ def get_ini(file_name, separator="=", encoding=None): class _Section(OrderedDict): - def __init__(self, name, inicontents="", separator="=", commenter="#", no_spaces=False): + def __init__( + self, name, inicontents="", separator="=", commenter="#", no_spaces=False + ): super().__init__(self) self.name = name self.inicontents = inicontents self.sep = separator self.com = commenter - if not no_spaces = - self.sep = ' ' + self.sep + ' ' + self.no_spaces = no_spaces opt_regx_prefix = r"(\s*)(.+?)\s*" opt_regx_suffix = r"\s*(.*)\s*" - self.opt_regx_str = r"{}(\{}){}".format( - opt_regx_prefix, self.sep, opt_regx_suffix - ) + self.opt_regx_str = rf"{opt_regx_prefix}(\{self.sep}){opt_regx_suffix}" self.opt_regx = re.compile(self.opt_regx_str) def refresh(self, inicontents=None): @@ -477,7 +486,11 @@ class _Section(OrderedDict): # Ensure the value is either a _Section or a string if isinstance(value, (dict, OrderedDict)): sect = _Section( - name=key, inicontents="", separator=self.sep, commenter=self.com + name=key, + inicontents="", + separator=self.sep, + commenter=self.com, + no_spaces=self.no_spaces, ) sect.update(value) value = sect @@ -509,7 +522,7 @@ class _Section(OrderedDict): return changes def gen_ini(self): - yield "{0}[{1}]{0}".format(os.linesep, self.name) + yield f"{os.linesep}[{self.name}]{os.linesep}" sections_dict = OrderedDict() for name, value in self.items(): # Handle Comment Lines @@ -520,12 +533,19 @@ class _Section(OrderedDict): sections_dict.update({name: value}) # Key / Value pairs else: - yield "{}{}{}{}".format( - name, - self.sep, - value, - os.linesep, - ) + # multiple spaces will be a single space + if all(c == " " for c in self.sep): + self.sep = " " + # Default is to add spaces + if self.no_spaces: + if self.sep != " ": + # We only strip whitespace if the delimiter is not a space + self.sep = self.sep.strip() + else: + if self.sep != " ": + # We only add spaces if the delimiter itself is not a space + self.sep = f" {self.sep.strip()} " + yield f"{name}{self.sep}{value}{os.linesep}" for name, value in sections_dict.items(): yield from value.gen_ini() @@ -558,15 +578,26 @@ class _Section(OrderedDict): class _Ini(_Section): def __init__( - self, name, inicontents="", separator="=", commenter="#", encoding=None + self, + name, + inicontents="", + separator="=", + commenter="#", + encoding=None, + no_spaces=False, ): super().__init__( - self, inicontents=inicontents, separator=separator, commenter=commenter + self, + inicontents=inicontents, + separator=separator, + commenter=commenter, + no_spaces=no_spaces, ) self.name = name if encoding is None: encoding = __salt_system_encoding__ self.encoding = encoding + self.no_spaces = no_spaces def refresh(self, inicontents=None): if inicontents is None: @@ -613,7 +644,7 @@ class _Ini(_Section): self.name, "w", encoding=self.encoding ) as outfile: ini_gen = self.gen_ini() - next(ini_gen) + next(ini_gen) # Next to skip the file name ini_gen_list = list(ini_gen) # Avoid writing an initial line separator. if ini_gen_list: @@ -625,8 +656,10 @@ class _Ini(_Section): ) @staticmethod - def get_ini_file(file_name, separator="=", encoding=None): - inifile = _Ini(file_name, separator=separator, encoding=encoding) + def get_ini_file(file_name, separator="=", encoding=None, no_spaces=False): + inifile = _Ini( + file_name, separator=separator, encoding=encoding, no_spaces=no_spaces + ) inifile.refresh() return inifile diff --git a/tests/pytests/unit/modules/test_ini_manage.py b/tests/pytests/unit/modules/test_ini_manage.py index 499bae71e06..e226f34dfaa 100644 --- a/tests/pytests/unit/modules/test_ini_manage.py +++ b/tests/pytests/unit/modules/test_ini_manage.py @@ -94,24 +94,22 @@ def test_get_option(encoding, linesep, ini_file, ini_content): ) ini_file.write_bytes(content) - assert ( - ini.get_option(str(ini_file), "main", "test1", encoding=encoding) == "value 1" - ) - assert ( - ini.get_option(str(ini_file), "main", "test2", encoding=encoding) == "value 2" - ) - assert ( - ini.get_option(str(ini_file), "SectionB", "test1", encoding=encoding) - == "value 1B" - ) - assert ( - ini.get_option(str(ini_file), "SectionB", "test3", encoding=encoding) - == "value 3B" - ) - assert ( - ini.get_option(str(ini_file), "SectionC", "empty_option", encoding=encoding) - == "" + option = ini.get_option(str(ini_file), "main", "test1", encoding=encoding) + assert option == "value 1" + + option = ini.get_option(str(ini_file), "main", "test2", encoding=encoding) + assert option == "value 2" + + option = ini.get_option(str(ini_file), "SectionB", "test1", encoding=encoding) + assert option == "value 1B" + + option = ini.get_option(str(ini_file), "SectionB", "test3", encoding=encoding) + assert option == "value 3B" + + option = ini.get_option( + str(ini_file), "SectionC", "empty_option", encoding=encoding ) + assert option == "" @pytest.mark.parametrize("linesep", ["\r", "\n", "\r\n"]) @@ -249,11 +247,12 @@ def test_set_option(encoding, linesep, ini_file, ini_content): ) +@pytest.mark.parametrize("no_spaces", [True, False]) @pytest.mark.parametrize("linesep", ["\r", "\n", "\r\n"]) @pytest.mark.parametrize( "encoding", [None, "cp1252" if sys.platform == "win32" else "ISO-2022-JP"] ) -def test_empty_value(encoding, linesep, ini_file, ini_content): +def test_empty_value(encoding, linesep, no_spaces, ini_file, ini_content): """ Test empty value preserved after edit """ @@ -263,19 +262,23 @@ def test_empty_value(encoding, linesep, ini_file, ini_content): ini_file.write_bytes(content) ini.set_option( - str(ini_file), {"SectionB": {"test3": "new value 3B"}}, encoding=encoding + str(ini_file), + {"SectionB": {"test3": "new value 3B"}}, + encoding=encoding, + no_spaces=no_spaces, ) with salt.utils.files.fopen(str(ini_file), "r") as fp_: file_content = salt.utils.stringutils.to_unicode(fp_.read(), encoding=encoding) - expected = "{0}{1}{0}".format(os.linesep, "empty_option = ") + expected = f"{os.linesep}empty_option{'=' if no_spaces else ' = '}{os.linesep}" assert expected in file_content, "empty_option was not preserved" +@pytest.mark.parametrize("no_spaces", [True, False]) @pytest.mark.parametrize("linesep", ["\r", "\n", "\r\n"]) @pytest.mark.parametrize( "encoding", [None, "cp1252" if sys.platform == "win32" else "ISO-2022-JP"] ) -def test_empty_lines(encoding, linesep, ini_file, ini_content): +def test_empty_lines(encoding, linesep, no_spaces, ini_file, ini_content): """ Test empty lines preserved after edit """ @@ -289,42 +292,48 @@ def test_empty_lines(encoding, linesep, ini_file, ini_content): "# Comment on the first line", "", "# First main option", - "option1 = main1", + f"option1{'=' if no_spaces else ' = '}main1", "", "# Second main option", - "option2 = main2", + f"option2{'=' if no_spaces else ' = '}main2", "", "[main]", "# Another comment", - "test1 = value 1", + f"test1{'=' if no_spaces else ' = '}value 1", "", - "test2 = value 2", + f"test2{'=' if no_spaces else ' = '}value 2", "", "[SectionB]", - "test1 = value 1B", + f"test1{'=' if no_spaces else ' = '}value 1B", "", "# Blank line should be above", - "test3 = new value 3B", + f"test3{'=' if no_spaces else ' = '}new value 3B", "", "[SectionC]", "# The following option is empty", - "empty_option = ", + f"empty_option{'=' if no_spaces else ' = '}", "", ] ) ini.set_option( - str(ini_file), {"SectionB": {"test3": "new value 3B"}}, encoding=encoding + str(ini_file), + {"SectionB": {"test3": "new value 3B"}}, + encoding=encoding, + no_spaces=no_spaces, ) with salt.utils.files.fopen(str(ini_file), "r") as fp_: file_content = fp_.read() assert expected == file_content +@pytest.mark.parametrize("no_spaces", [True, False]) @pytest.mark.parametrize("linesep", ["\r", "\n", "\r\n"]) @pytest.mark.parametrize( "encoding", [None, "cp1252" if sys.platform == "win32" else "ISO-2022-JP"] ) -def test_empty_lines_multiple_edits(encoding, linesep, ini_file, ini_content): +def test_empty_lines_multiple_edits( + encoding, linesep, no_spaces, ini_file, ini_content +): """ Test empty lines preserved after multiple edits """ @@ -337,6 +346,7 @@ def test_empty_lines_multiple_edits(encoding, linesep, ini_file, ini_content): str(ini_file), {"SectionB": {"test3": "this value will be edited two times"}}, encoding=encoding, + no_spaces=no_spaces, ) expected = os.linesep.join( @@ -344,31 +354,34 @@ def test_empty_lines_multiple_edits(encoding, linesep, ini_file, ini_content): "# Comment on the first line", "", "# First main option", - "option1 = main1", + f"option1{'=' if no_spaces else ' = '}main1", "", "# Second main option", - "option2 = main2", + f"option2{'=' if no_spaces else ' = '}main2", "", "[main]", "# Another comment", - "test1 = value 1", + f"test1{'=' if no_spaces else ' = '}value 1", "", - "test2 = value 2", + f"test2{'=' if no_spaces else ' = '}value 2", "", "[SectionB]", - "test1 = value 1B", + f"test1{'=' if no_spaces else ' = '}value 1B", "", "# Blank line should be above", - "test3 = new value 3B", + f"test3{'=' if no_spaces else ' = '}new value 3B", "", "[SectionC]", "# The following option is empty", - "empty_option = ", + f"empty_option{'=' if no_spaces else ' = '}", "", ] ) ini.set_option( - str(ini_file), {"SectionB": {"test3": "new value 3B"}}, encoding=encoding + str(ini_file), + {"SectionB": {"test3": "new value 3B"}}, + encoding=encoding, + no_spaces=no_spaces, ) with salt.utils.files.fopen(str(ini_file), "r") as fp_: file_content = fp_.read()