Add an option to ini.set_option

Adds an new option named ``no_spaces`` to the ini.set_option function
that allows the user to deteremine whether the separator (``=``) should
be wrapped with spaces or not.
This commit is contained in:
twangboy 2024-08-21 14:15:23 -06:00 committed by Daniel Wozniak
parent 3ae69b9691
commit 7faee48ef3
3 changed files with 108 additions and 59 deletions

3
changelog/33669.added.md Normal file
View file

@ -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.

View file

@ -37,7 +37,7 @@ COM_REGX = re.compile(r"^\s*(#|;)\s*(.*)")
INDENTED_REGX = re.compile(r"(\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 Edit an ini file, replacing one or more sections. Returns a dictionary
containing the changes made. containing the changes made.
@ -66,6 +66,14 @@ def set_option(file_name, sections=None, separator="=", encoding=None):
.. versionadded:: 3006.6 .. 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: Returns:
dict: A dictionary representing the changes made to the ini file 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 {} 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) changes = inifile.update(sections)
inifile.flush() inifile.flush()
return changes return changes
@ -388,20 +398,19 @@ def get_ini(file_name, separator="=", encoding=None):
class _Section(OrderedDict): 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) super().__init__(self)
self.name = name self.name = name
self.inicontents = inicontents self.inicontents = inicontents
self.sep = separator self.sep = separator
self.com = commenter self.com = commenter
if not no_spaces = self.no_spaces = no_spaces
self.sep = ' ' + self.sep + ' '
opt_regx_prefix = r"(\s*)(.+?)\s*" opt_regx_prefix = r"(\s*)(.+?)\s*"
opt_regx_suffix = r"\s*(.*)\s*" opt_regx_suffix = r"\s*(.*)\s*"
self.opt_regx_str = r"{}(\{}){}".format( self.opt_regx_str = rf"{opt_regx_prefix}(\{self.sep}){opt_regx_suffix}"
opt_regx_prefix, self.sep, opt_regx_suffix
)
self.opt_regx = re.compile(self.opt_regx_str) self.opt_regx = re.compile(self.opt_regx_str)
def refresh(self, inicontents=None): def refresh(self, inicontents=None):
@ -477,7 +486,11 @@ class _Section(OrderedDict):
# Ensure the value is either a _Section or a string # Ensure the value is either a _Section or a string
if isinstance(value, (dict, OrderedDict)): if isinstance(value, (dict, OrderedDict)):
sect = _Section( 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) sect.update(value)
value = sect value = sect
@ -509,7 +522,7 @@ class _Section(OrderedDict):
return changes return changes
def gen_ini(self): def gen_ini(self):
yield "{0}[{1}]{0}".format(os.linesep, self.name) yield f"{os.linesep}[{self.name}]{os.linesep}"
sections_dict = OrderedDict() sections_dict = OrderedDict()
for name, value in self.items(): for name, value in self.items():
# Handle Comment Lines # Handle Comment Lines
@ -520,12 +533,19 @@ class _Section(OrderedDict):
sections_dict.update({name: value}) sections_dict.update({name: value})
# Key / Value pairs # Key / Value pairs
else: else:
yield "{}{}{}{}".format( # multiple spaces will be a single space
name, if all(c == " " for c in self.sep):
self.sep, self.sep = " "
value, # Default is to add spaces
os.linesep, 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(): for name, value in sections_dict.items():
yield from value.gen_ini() yield from value.gen_ini()
@ -558,15 +578,26 @@ class _Section(OrderedDict):
class _Ini(_Section): class _Ini(_Section):
def __init__( def __init__(
self, name, inicontents="", separator="=", commenter="#", encoding=None self,
name,
inicontents="",
separator="=",
commenter="#",
encoding=None,
no_spaces=False,
): ):
super().__init__( super().__init__(
self, inicontents=inicontents, separator=separator, commenter=commenter self,
inicontents=inicontents,
separator=separator,
commenter=commenter,
no_spaces=no_spaces,
) )
self.name = name self.name = name
if encoding is None: if encoding is None:
encoding = __salt_system_encoding__ encoding = __salt_system_encoding__
self.encoding = encoding self.encoding = encoding
self.no_spaces = no_spaces
def refresh(self, inicontents=None): def refresh(self, inicontents=None):
if inicontents is None: if inicontents is None:
@ -613,7 +644,7 @@ class _Ini(_Section):
self.name, "w", encoding=self.encoding self.name, "w", encoding=self.encoding
) as outfile: ) as outfile:
ini_gen = self.gen_ini() ini_gen = self.gen_ini()
next(ini_gen) next(ini_gen) # Next to skip the file name
ini_gen_list = list(ini_gen) ini_gen_list = list(ini_gen)
# Avoid writing an initial line separator. # Avoid writing an initial line separator.
if ini_gen_list: if ini_gen_list:
@ -625,8 +656,10 @@ class _Ini(_Section):
) )
@staticmethod @staticmethod
def get_ini_file(file_name, separator="=", encoding=None): def get_ini_file(file_name, separator="=", encoding=None, no_spaces=False):
inifile = _Ini(file_name, separator=separator, encoding=encoding) inifile = _Ini(
file_name, separator=separator, encoding=encoding, no_spaces=no_spaces
)
inifile.refresh() inifile.refresh()
return inifile return inifile

View file

@ -94,24 +94,22 @@ def test_get_option(encoding, linesep, ini_file, ini_content):
) )
ini_file.write_bytes(content) ini_file.write_bytes(content)
assert ( option = ini.get_option(str(ini_file), "main", "test1", encoding=encoding)
ini.get_option(str(ini_file), "main", "test1", encoding=encoding) == "value 1" assert option == "value 1"
)
assert ( option = ini.get_option(str(ini_file), "main", "test2", encoding=encoding)
ini.get_option(str(ini_file), "main", "test2", encoding=encoding) == "value 2" assert option == "value 2"
)
assert ( option = ini.get_option(str(ini_file), "SectionB", "test1", encoding=encoding)
ini.get_option(str(ini_file), "SectionB", "test1", encoding=encoding) assert option == "value 1B"
== "value 1B"
) option = ini.get_option(str(ini_file), "SectionB", "test3", encoding=encoding)
assert ( assert option == "value 3B"
ini.get_option(str(ini_file), "SectionB", "test3", encoding=encoding)
== "value 3B" option = ini.get_option(
) str(ini_file), "SectionC", "empty_option", encoding=encoding
assert (
ini.get_option(str(ini_file), "SectionC", "empty_option", encoding=encoding)
== ""
) )
assert option == ""
@pytest.mark.parametrize("linesep", ["\r", "\n", "\r\n"]) @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("linesep", ["\r", "\n", "\r\n"])
@pytest.mark.parametrize( @pytest.mark.parametrize(
"encoding", [None, "cp1252" if sys.platform == "win32" else "ISO-2022-JP"] "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 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_file.write_bytes(content)
ini.set_option( 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_: with salt.utils.files.fopen(str(ini_file), "r") as fp_:
file_content = salt.utils.stringutils.to_unicode(fp_.read(), encoding=encoding) 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" 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("linesep", ["\r", "\n", "\r\n"])
@pytest.mark.parametrize( @pytest.mark.parametrize(
"encoding", [None, "cp1252" if sys.platform == "win32" else "ISO-2022-JP"] "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 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", "# Comment on the first line",
"", "",
"# First main option", "# First main option",
"option1 = main1", f"option1{'=' if no_spaces else ' = '}main1",
"", "",
"# Second main option", "# Second main option",
"option2 = main2", f"option2{'=' if no_spaces else ' = '}main2",
"", "",
"[main]", "[main]",
"# Another comment", "# 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]", "[SectionB]",
"test1 = value 1B", f"test1{'=' if no_spaces else ' = '}value 1B",
"", "",
"# Blank line should be above", "# Blank line should be above",
"test3 = new value 3B", f"test3{'=' if no_spaces else ' = '}new value 3B",
"", "",
"[SectionC]", "[SectionC]",
"# The following option is empty", "# The following option is empty",
"empty_option = ", f"empty_option{'=' if no_spaces else ' = '}",
"", "",
] ]
) )
ini.set_option( 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_: with salt.utils.files.fopen(str(ini_file), "r") as fp_:
file_content = fp_.read() file_content = fp_.read()
assert expected == file_content assert expected == file_content
@pytest.mark.parametrize("no_spaces", [True, False])
@pytest.mark.parametrize("linesep", ["\r", "\n", "\r\n"]) @pytest.mark.parametrize("linesep", ["\r", "\n", "\r\n"])
@pytest.mark.parametrize( @pytest.mark.parametrize(
"encoding", [None, "cp1252" if sys.platform == "win32" else "ISO-2022-JP"] "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 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), str(ini_file),
{"SectionB": {"test3": "this value will be edited two times"}}, {"SectionB": {"test3": "this value will be edited two times"}},
encoding=encoding, encoding=encoding,
no_spaces=no_spaces,
) )
expected = os.linesep.join( 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", "# Comment on the first line",
"", "",
"# First main option", "# First main option",
"option1 = main1", f"option1{'=' if no_spaces else ' = '}main1",
"", "",
"# Second main option", "# Second main option",
"option2 = main2", f"option2{'=' if no_spaces else ' = '}main2",
"", "",
"[main]", "[main]",
"# Another comment", "# 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]", "[SectionB]",
"test1 = value 1B", f"test1{'=' if no_spaces else ' = '}value 1B",
"", "",
"# Blank line should be above", "# Blank line should be above",
"test3 = new value 3B", f"test3{'=' if no_spaces else ' = '}new value 3B",
"", "",
"[SectionC]", "[SectionC]",
"# The following option is empty", "# The following option is empty",
"empty_option = ", f"empty_option{'=' if no_spaces else ' = '}",
"", "",
] ]
) )
ini.set_option( 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_: with salt.utils.files.fopen(str(ini_file), "r") as fp_:
file_content = fp_.read() file_content = fp_.read()