fix: update scp integration for more robust config handling (#535)

* chore: remove scp

* Squashed 'kiauh/core/submodules/simple_config_parser/' content from commit abee21c

git-subtree-dir: kiauh/core/submodules/simple_config_parser
git-subtree-split: abee21c08658be4529028844304df60650c09afa

* Squashed 'kiauh/core/submodules/simple_config_parser/' changes from abee21c..aa0302b

aa0302b fix: fix missing newline chars in raw strings

git-subtree-dir: kiauh/core/submodules/simple_config_parser
git-subtree-split: aa0302b02b56b252ed88fd2db88ee878a5bb7b5b

* Squashed 'kiauh/core/submodules/simple_config_parser/' changes from aa0302b..ef52958

ef52958 refactor: conditionally add empty line when adding new section

git-subtree-dir: kiauh/core/submodules/simple_config_parser
git-subtree-split: ef529580f469ef020135cb03e250fcd4e0d70acf

* fix: update scp integration for more robust cfg modification

Signed-off-by: Dominik Willner <th33xitus@gmail.com>

---------

Signed-off-by: Dominik Willner <th33xitus@gmail.com>
This commit is contained in:
dw-0
2024-09-21 13:55:30 +02:00
committed by GitHub
parent b604d93d0c
commit 6b6607c5ab
58 changed files with 3103 additions and 1184 deletions

View File

@@ -174,8 +174,8 @@ def create_example_printer_cfg(
return
scp = SimpleConfigParser()
scp.read(target)
scp.set("virtual_sdcard", "path", str(instance.base.gcodes_dir))
scp.read_file(target)
scp.set_option("virtual_sdcard", "path", str(instance.base.gcodes_dir))
# include existing client configs in the example config
if clients is not None and len(clients) > 0:
@@ -185,7 +185,7 @@ def create_example_printer_cfg(
scp.add_section(section=section)
create_client_config_symlink(client_config, [instance])
scp.write(target)
scp.write_file(target)
Logger.print_ok(f"Example printer.cfg created in '{instance.base.cfg_dir}'")

View File

@@ -138,7 +138,7 @@ class Moonraker:
return None
scp = SimpleConfigParser()
scp.read(self.cfg_file)
scp.read_file(self.cfg_file)
port: int | None = scp.getint("server", "port", fallback=None)
return port

View File

@@ -77,20 +77,15 @@ def create_example_moonraker_conf(
uds = instance.base.comms_dir.joinpath("klippy.sock")
scp = SimpleConfigParser()
scp.read(target)
scp.read_file(target)
trusted_clients: List[str] = [
".".join(ip),
*scp.get("authorization", "trusted_clients"),
f" {'.'.join(ip)}\n",
*scp.getval("authorization", "trusted_clients"),
]
scp.set("server", "port", str(port))
scp.set("server", "klippy_uds_address", str(uds))
scp.set(
"authorization",
"trusted_clients",
"\n".join(trusted_clients),
True,
)
scp.set_option("server", "port", str(port))
scp.set_option("server", "klippy_uds_address", str(uds))
scp.set_option("authorization", "trusted_clients", trusted_clients)
# add existing client and client configs in the update section
if clients is not None and len(clients) > 0:
@@ -105,7 +100,7 @@ def create_example_moonraker_conf(
]
scp.add_section(section=c_section)
for option in c_options:
scp.set(c_section, option[0], option[1])
scp.set_option(c_section, option[0], option[1])
# client config part
c_config = c.client_config
@@ -120,9 +115,9 @@ def create_example_moonraker_conf(
]
scp.add_section(section=c_config_section)
for option in c_config_options:
scp.set(c_config_section, option[0], option[1])
scp.set_option(c_config_section, option[0], option[1])
scp.write(target)
scp.write_file(target)
Logger.print_ok(f"Example moonraker.conf created in '{instance.base.cfg_dir}'")

View File

@@ -45,7 +45,6 @@ class RemoveMenu(BaseMenu):
"4": Option(method=self.remove_fluidd),
"5": Option(method=self.remove_klipperscreen),
"6": Option(method=self.remove_crowsnest),
"7": Option(method=self.remove_octoeverywhere),
}
def print_menu(self) -> None:

View File

@@ -53,7 +53,11 @@ class KiauhSettings:
return cls._instance
def __repr__(self) -> str:
return f"KiauhSettings(kiauh={self.kiauh}, klipper={self.klipper}, moonraker={self.moonraker}, mainsail={self.mainsail}, fluidd={self.fluidd})"
return (
f"KiauhSettings(kiauh={self.kiauh}, klipper={self.klipper},"
f" moonraker={self.moonraker}, mainsail={self.mainsail},"
f" fluidd={self.fluidd})"
)
def __getitem__(self, item: str) -> Any:
return getattr(self, item)
@@ -75,8 +79,8 @@ class KiauhSettings:
def get(self, section: str, option: str) -> str | int | bool:
"""
Get a value from the settings state by providing the section and option name as strings.
Prefer direct access to the properties, as it is usually safer!
Get a value from the settings state by providing the section and option name as
strings. Prefer direct access to the properties, as it is usually safer!
:param section: The section name as string.
:param option: The option name as string.
:return: The value of the option as string, int or bool.
@@ -91,7 +95,7 @@ class KiauhSettings:
def save(self) -> None:
self._set_config_options_state()
self.config.write(CUSTOM_CFG)
self.config.write_file(CUSTOM_CFG)
self._load_config()
def _load_config(self) -> None:
@@ -99,7 +103,7 @@ class KiauhSettings:
self._kill()
cfg = CUSTOM_CFG if CUSTOM_CFG.exists() else DEFAULT_CFG
self.config.read(cfg)
self.config.read_file(cfg)
self._validate_cfg()
self._apply_settings_from_file()
@@ -132,7 +136,7 @@ class KiauhSettings:
def _validate_bool(self, section: str, option: str) -> None:
self._v_section, self._v_option = (section, option)
bool(self.config.getboolean(section, option))
(bool(self.config.getboolean(section, option)))
def _validate_int(self, section: str, option: str) -> None:
self._v_section, self._v_option = (section, option)
@@ -140,7 +144,7 @@ class KiauhSettings:
def _validate_str(self, section: str, option: str) -> None:
self._v_section, self._v_option = (section, option)
v = self.config.get(section, option)
v = self.config.getval(section, option)
if v.isdigit() or v.lower() == "true" or v.lower() == "false":
raise ValueError
@@ -148,10 +152,10 @@ class KiauhSettings:
self.kiauh.backup_before_update = self.config.getboolean(
"kiauh", "backup_before_update"
)
self.klipper.repo_url = self.config.get("klipper", "repo_url")
self.klipper.branch = self.config.get("klipper", "branch")
self.moonraker.repo_url = self.config.get("moonraker", "repo_url")
self.moonraker.branch = self.config.get("moonraker", "branch")
self.klipper.repo_url = self.config.getval("klipper", "repo_url")
self.klipper.branch = self.config.getval("klipper", "branch")
self.moonraker.repo_url = self.config.getval("moonraker", "repo_url")
self.moonraker.branch = self.config.getval("moonraker", "branch")
self.mainsail.port = self.config.getint("mainsail", "port")
self.mainsail.unstable_releases = self.config.getboolean(
"mainsail", "unstable_releases"
@@ -162,23 +166,23 @@ class KiauhSettings:
)
def _set_config_options_state(self) -> None:
self.config.set(
self.config.set_option(
"kiauh",
"backup_before_update",
str(self.kiauh.backup_before_update),
)
self.config.set("klipper", "repo_url", self.klipper.repo_url)
self.config.set("klipper", "branch", self.klipper.branch)
self.config.set("moonraker", "repo_url", self.moonraker.repo_url)
self.config.set("moonraker", "branch", self.moonraker.branch)
self.config.set("mainsail", "port", str(self.mainsail.port))
self.config.set(
self.config.set_option("klipper", "repo_url", self.klipper.repo_url)
self.config.set_option("klipper", "branch", self.klipper.branch)
self.config.set_option("moonraker", "repo_url", self.moonraker.repo_url)
self.config.set_option("moonraker", "branch", self.moonraker.branch)
self.config.set_option("mainsail", "port", str(self.mainsail.port))
self.config.set_option(
"mainsail",
"unstable_releases",
str(self.mainsail.unstable_releases),
)
self.config.set("fluidd", "port", str(self.fluidd.port))
self.config.set(
self.config.set_option("fluidd", "port", str(self.fluidd.port))
self.config.set_option(
"fluidd", "unstable_releases", str(self.fluidd.unstable_releases)
)

View File

@@ -0,0 +1,62 @@
# ======================================================================= #
# Copyright (C) 2024 Dominik Willner <th33xitus@gmail.com> #
# #
# https://github.com/dw-0/simple-config-parser #
# #
# This file may be distributed under the terms of the GNU GPLv3 license #
# ======================================================================= #
import re
# definition of section line:
# - then line MUST start with an opening square bracket - it is the first section marker
# - the section marker MUST be followed by at least one character - it is the section name
# - the section name MUST be followed by a closing square bracket - it is the second section marker
# - the second section marker MAY be followed by any amount of whitespace characters
# - the second section marker MAY be followed by a # or ; - it is the comment marker
# - the inline comment MAY be of any length and character
SECTION_RE = re.compile(r"^\[(\S.*\S|\S)]\s*([#;].*)?$")
# definition of option line:
# - the line MUST start with a word - it is the option name
# - the option name MUST be followed by a colon or an equal sign - it is the separator
# - the separator MUST be followed by a value
# - the separator MAY have any amount of leading or trailing whitespaces
# - the separator MUST NOT be directly followed by a colon or equal sign
# - the value MAY be of any length and character
# - the value MAY contain any amount of trailing whitespaces
# - the value MAY be followed by a # or ; - it is the comment marker
# - the inline comment MAY be of any length and character
OPTION_RE = re.compile(r"^([^;#:=\s]+)\s?[:=]\s*([^;#:=\s][^;#]*?)\s*([#;].*)?$")
# definition of options block start line:
# - the line MUST start with a word - it is the option name
# - the option name MUST be followed by a colon or an equal sign - it is the separator
# - the separator MUST NOT be followed by a value
# - the separator MAY have any amount of leading or trailing whitespaces
# - the separator MUST NOT be directly followed by a colon or equal sign
# - the separator MAY be followed by a # or ; - it is the comment marker
# - the inline comment MAY be of any length and character
OPTIONS_BLOCK_START_RE = re.compile(r"^([^;#:=\s]+)\s*[:=]\s*([#;].*)?$")
# definition of comment line:
# - the line MAY start with any amount of whitespace characters
# - the line MUST contain a # or ; - it is the comment marker
# - the comment marker MAY be followed by any amount of whitespace characters
# - the comment MAY be of any length and character
LINE_COMMENT_RE = re.compile(r"^\s*[#;].*")
# definition of empty line:
# - the line MUST contain only whitespace characters
EMPTY_LINE_RE = re.compile(r"^\s*$")
BOOLEAN_STATES = {
"1": True,
"yes": True,
"true": True,
"on": True,
"0": False,
"no": False,
"false": False,
"off": False,
}
HEADER_IDENT = "#_header"

View File

@@ -1,5 +1,5 @@
# ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
# Copyright (C) 2024 Dominik Willner <th33xitus@gmail.com> #
# #
# https://github.com/dw-0/simple-config-parser #
# #
@@ -8,47 +8,24 @@
from __future__ import annotations
import re
import secrets
import string
from pathlib import Path
from typing import Callable, Dict, List, Match, Tuple, TypedDict
from typing import Callable, Dict, List
from ..simple_config_parser.constants import (
BOOLEAN_STATES,
EMPTY_LINE_RE,
HEADER_IDENT,
LINE_COMMENT_RE,
OPTION_RE,
OPTIONS_BLOCK_START_RE,
SECTION_RE,
)
_UNSET = object()
class Section(TypedDict):
"""
A single section in the config file
- _raw: The raw representation of the section name
- options: A list of options in the section
"""
_raw: str
options: List[Option]
class Option(TypedDict, total=False):
"""
A single option in a section in the config file
- is_multiline: Whether the option is a multiline option
- option: The name of the option
- value: The value of the option
- _raw: The raw representation of the option
- _raw_value: The raw value of the option
A multinline option is an option that contains multiple lines of text following
the option name in the next line. The value of a multiline option is a list of
strings, where each string represents a single line of text.
"""
is_multiline: bool
option: str
value: str | List[str]
_raw: str
_raw_value: str | List[str]
class NoSectionError(Exception):
"""Raised when a section is not defined"""
@@ -57,14 +34,6 @@ class NoSectionError(Exception):
super().__init__(msg)
class NoOptionError(Exception):
"""Raised when an option is not defined in a section"""
def __init__(self, option: str, section: str):
msg = f"Option '{option}' in section '{section}' is not defined"
super().__init__(msg)
class DuplicateSectionError(Exception):
"""Raised when a section is defined more than once"""
@@ -73,11 +42,11 @@ class DuplicateSectionError(Exception):
super().__init__(msg)
class DuplicateOptionError(Exception):
"""Raised when an option is defined more than once"""
class NoOptionError(Exception):
"""Raised when an option is not defined in a section"""
def __init__(self, option: str, section: str):
msg = f"Option '{option}' in section '{section}' is defined more than once"
msg = f"Option '{option}' in section '{section}' is not defined"
super().__init__(msg)
@@ -85,159 +54,195 @@ class DuplicateOptionError(Exception):
class SimpleConfigParser:
"""A customized config parser targeted at handling Klipper style config files"""
# definition of section line:
# - then line MUST start with an opening square bracket - it is the first section marker
# - the section marker MUST be followed by at least one character - it is the section name
# - the section name MUST be followed by a closing square bracket - it is the second section marker
# - the second section marker MAY be followed by any amount of whitespace characters
# - the second section marker MAY be followed by a # or ; - it is the comment marker
# - the inline comment MAY be of any length and character
_SECTION_RE = re.compile(r"\[(.+)]\s*([#;].*)?$")
def __init__(self) -> None:
self.header: List[str] = []
self.config: Dict = {}
self.current_section: str | None = None
self.current_opt_block: str | None = None
self.current_collector: str | None = None
self.in_option_block: bool = False
# definition of option line:
# - the line MUST start with a word - it is the option name
# - the option name MUST be followed by a colon or an equal sign - it is the separator
# - the separator MUST be followed by a value
# - the separator MAY have any amount of leading or trailing whitespaces
# - the separator MUST NOT be directly followed by a colon or equal sign
# - the value MAY be of any length and character
# - the value MAY contain any amount of trailing whitespaces
# - the value MAY be followed by a # or ; - it is the comment marker
# - the inline comment MAY be of any length and character
_OPTION_RE = re.compile(r"^([^:=\s]+)\s?[:=]\s*([^=:].*)\s*([#;].*)?$")
def _match_section(self, line: str) -> bool:
"""Wheter or not the given line matches the definition of a section"""
return SECTION_RE.match(line) is not None
# definition of multiline option line:
# - the line MUST start with a word - it is the option name
# - the option name MUST be followed by a colon or an equal sign - it is the separator
# - the separator MUST NOT be followed by a value
# - the separator MAY have any amount of leading or trailing whitespaces
# - the separator MUST NOT be directly followed by a colon or equal sign
# - the separator MAY be followed by a # or ; - it is the comment marker
# - the inline comment MAY be of any length and character
_MLOPTION_RE = re.compile(r"^([^:=\s]+)\s*[:=]\s*([#;].*)?$")
def _match_option(self, line: str) -> bool:
"""Wheter or not the given line matches the definition of an option"""
return OPTION_RE.match(line) is not None
# definition of comment line:
# - the line MAY start with any amount of whitespace characters
# - the line MUST contain a # or ; - it is the comment marker
# - the comment marker MAY be followed by any amount of whitespace characters
# - the comment MAY be of any length and character
_COMMENT_RE = re.compile(r"^\s*([#;].*)?$")
def _match_options_block_start(self, line: str) -> bool:
"""Wheter or not the given line matches the definition of a multiline option"""
return OPTIONS_BLOCK_START_RE.match(line) is not None
# definition of empty line:
# - the line MUST contain only whitespace characters
_EMPTY_LINE_RE = re.compile(r"^\s*$")
def _match_line_comment(self, line: str) -> bool:
"""Wheter or not the given line matches the definition of a comment"""
return LINE_COMMENT_RE.match(line) is not None
BOOLEAN_STATES = {
"1": True,
"yes": True,
"true": True,
"on": True,
"0": False,
"no": False,
"false": False,
"off": False,
}
def _match_empty_line(self, line: str) -> bool:
"""Wheter or not the given line matches the definition of an empty line"""
return EMPTY_LINE_RE.match(line) is not None
def __init__(self):
self._config: Dict = {}
self._header: List[str] = []
self._all_sections: List[str] = []
self._all_options: Dict = {}
self.section_name: str = ""
self.in_option_block: bool = False # whether we are in a multiline option block
def _parse_line(self, line: str) -> None:
"""Parses a line and determines its type"""
if self._match_section(line):
self.current_collector = None
self.current_opt_block = None
self.current_section = SECTION_RE.match(line).group(1)
self.config[self.current_section] = {"_raw": line}
def read(self, file: Path) -> None:
"""
Read the given file and store the result in the internal state.
Call this method before using any other methods. Calling this method
multiple times will reset the internal state on each call.
"""
elif self._match_option(line):
self.current_collector = None
self.current_opt_block = None
option = OPTION_RE.match(line).group(1)
value = OPTION_RE.match(line).group(2)
self.config[self.current_section][option] = {"_raw": line, "value": value}
self._reset_state()
elif self._match_options_block_start(line):
self.current_collector = None
option = OPTIONS_BLOCK_START_RE.match(line).group(1)
self.current_opt_block = option
self.config[self.current_section][option] = {"_raw": line, "value": []}
try:
with open(file, "r") as f:
self._parse_config(f.readlines())
elif self.current_opt_block is not None:
self.config[self.current_section][self.current_opt_block]["value"].append(
line
)
except OSError:
raise
elif self._match_empty_line(line) or self._match_line_comment(line):
self.current_opt_block = None
def _reset_state(self):
"""Reset the internal state."""
# if current_section is None, we are at the beginning of the file,
# so we consider the part up to the first section as the file header
if not self.current_section:
self.config.setdefault(HEADER_IDENT, []).append(line)
else:
section = self.config[self.current_section]
self._config.clear()
self._header.clear()
self._all_sections.clear()
self._all_options.clear()
self.section_name = ""
self.in_option_block = False
# set the current collector to a new value, so that continuous
# empty lines or comments are collected into the same collector
if not self.current_collector:
self.current_collector = self._generate_rand_id()
section[self.current_collector] = []
def write(self, filename):
"""Write the internal state to the given file"""
section[self.current_collector].append(line)
content = self._construct_content()
def read_file(self, file: Path) -> None:
"""Read and parse a config file"""
with open(file, "r") as file:
for line in file:
self._parse_line(line)
with open(filename, "w") as f:
f.write(content)
# print(json.dumps(self.config, indent=4))
def _construct_content(self) -> str:
"""
Constructs the content of the configuration file based on the internal state of
the _config object by iterating over the sections and their options. It starts
by checking if a header is present and extends the content list with its elements.
Then, for each section, it appends the raw representation of the section to the
content list. If the section has a body, it iterates over its options and extends
the content list with their raw representations. If an option is multiline, it
also extends the content list with its raw value. Finally, the content list is
joined into a single string and returned.
def write_file(self, file: Path) -> None:
"""Write the current config to the config file"""
if not file:
raise ValueError("No config file specified")
:return: The content of the configuration file as a string
"""
content: List[str] = []
if self._header is not None:
content.extend(self._header)
for section in self._config:
content.append(self._config[section]["_raw"])
with open(file, "w") as file:
self._write_header(file)
self._write_sections(file)
if (sec_body := self._config[section].get("body")) is not None:
for option in sec_body:
content.extend(option["_raw"])
if option["is_multiline"]:
content.extend(option["_raw_value"])
content: str = "".join(content)
def _write_header(self, file) -> None:
"""Write the header to the config file"""
for line in self.config.get(HEADER_IDENT, []):
file.write(line)
return content
def _write_sections(self, file) -> None:
"""Write the sections to the config file"""
for section in self.get_sections():
for key, value in self.config[section].items():
self._write_section_content(file, key, value)
def sections(self) -> List[str]:
"""Return a list of section names"""
def _write_section_content(self, file, key, value) -> None:
"""Write the content of a section to the config file"""
if key == "_raw":
file.write(value)
elif key.startswith("#_"):
for line in value:
file.write(line)
elif isinstance(value["value"], list):
file.write(value["_raw"])
for line in value["value"]:
file.write(line)
else:
file.write(value["_raw"])
return self._all_sections
def get_sections(self) -> List[str]:
"""Return a list of all section names, but exclude any section starting with '#_'"""
return list(
filter(
lambda section: not section.startswith("#_"),
self.config.keys(),
)
)
def has_section(self, section: str) -> bool:
"""Check if a section exists"""
return section in self.get_sections()
def add_section(self, section: str) -> None:
"""Add a new section to the internal state"""
if section in self._all_sections:
"""Add a new section to the config"""
if section in self.get_sections():
raise DuplicateSectionError(section)
self._all_sections.append(section)
self._all_options[section] = {}
self._config[section] = {"_raw": f"\n[{section}]\n", "body": []}
if len(self.get_sections()) >= 1:
self._check_set_section_spacing()
self.config[section] = {"_raw": f"[{section}]\n"}
def _check_set_section_spacing(self):
prev_section = self.get_sections()[-1]
prev_section_content = self.config[prev_section]
last_item = list(prev_section_content.keys())[-1]
if last_item.startswith("#_") and last_item.keys()[-1] != "\n":
prev_section_content[last_item].append("\n")
else:
prev_section_content[self._generate_rand_id()] = ["\n"]
def remove_section(self, section: str) -> None:
"""Remove the given section"""
"""Remove a section from the config"""
self.config.pop(section, None)
if section not in self._all_sections:
raise NoSectionError(section)
def get_options(self, section: str) -> List[str]:
"""Return a list of all option names for a given section"""
return list(
filter(
lambda option: option != "_raw" and not option.startswith("#_"),
self.config[section].keys(),
)
)
self._all_sections.pop(self._all_sections.index(section))
self._all_options.pop(section)
self._config.pop(section)
def has_option(self, section: str, option: str) -> bool:
"""Check if an option exists in a section"""
return self.has_section(section) and option in self.get_options(section)
def options(self, section) -> List[str]:
"""Return a list of option names for the given section name"""
def set_option(self, section: str, option: str, value: str | List[str]) -> None:
"""
Set the value of an option in a section. If the section does not exist,
it is created. If the option does not exist, it is created.
"""
if not self.has_section(section):
self.add_section(section)
return self._all_options.get(section)
if not self.has_option(section, option):
self.config[section][option] = {
"_raw": f"{option}:\n"
if isinstance(value, list)
else f"{option}: {value}\n",
"value": value,
}
else:
opt = self.config[section][option]
if not isinstance(value, list):
opt["_raw"] = opt["_raw"].replace(opt["value"], value)
opt["value"] = value
def get(
def remove_option(self, section: str, option: str) -> None:
"""Remove an option from a section"""
self.config[section].pop(option, None)
def getval(
self, section: str, option: str, fallback: str | _UNSET = _UNSET
) -> str | List[str]:
"""
@@ -246,15 +251,12 @@ class SimpleConfigParser:
If the key is not found and 'fallback' is provided, it is used as
a fallback value.
"""
try:
if section not in self._all_sections:
if section not in self.get_sections():
raise NoSectionError(section)
if option not in self._all_options.get(section):
if option not in self.get_options(section):
raise NoOptionError(option, section)
return self._all_options[section][option]
return self.config[section][option]["value"]
except (NoSectionError, NoOptionError):
if fallback is _UNSET:
raise
@@ -262,25 +264,29 @@ class SimpleConfigParser:
def getint(self, section: str, option: str, fallback: int | _UNSET = _UNSET) -> int:
"""Return the value of the given option in the given section as an int"""
return self._get_conv(section, option, int, fallback=fallback)
def getfloat(
self, section: str, option: str, fallback: float | _UNSET = _UNSET
) -> float:
"""Return the value of the given option in the given section as a float"""
return self._get_conv(section, option, float, fallback=fallback)
def getboolean(
self, section: str, option: str, fallback: bool | _UNSET = _UNSET
) -> bool:
"""Return the value of the given option in the given section as a boolean"""
return self._get_conv(
section, option, self._convert_to_boolean, fallback=fallback
)
def _convert_to_boolean(self, value) -> bool:
if value.lower() not in self.BOOLEAN_STATES:
def _convert_to_boolean(self, value: str) -> bool:
"""Convert a string to a boolean"""
if isinstance(value, bool):
return value
if value.lower() not in BOOLEAN_STATES:
raise ValueError("Not a boolean: %s" % value)
return self.BOOLEAN_STATES[value.lower()]
return BOOLEAN_STATES[value.lower()]
def _get_conv(
self,
@@ -289,300 +295,18 @@ class SimpleConfigParser:
conv: Callable[[str], int | float | bool],
fallback: _UNSET = _UNSET,
) -> int | float | bool:
"""Return the value of the given option in the given section as a converted value"""
try:
return conv(self.get(section, option, fallback))
except:
return conv(self.getval(section, option, fallback))
except ValueError as e:
if fallback is not _UNSET:
return fallback
raise
def items(self, section: str) -> List[Tuple[str, str]]:
"""Return a list of (option, value) tuples for a specific section"""
if section not in self._all_sections:
raise NoSectionError(section)
result = []
for _option in self._all_options[section]:
result.append((_option, self._all_options[section][_option]))
return result
def set(
self,
section: str,
option: str,
value: str,
multiline: bool = False,
indent: int = 4,
) -> None:
"""Set the given option to the given value in the given section
If the option is already defined, it will be overwritten. If the option
is not defined yet, it will be added to the section body.
The multiline parameter can be used to specify whether the value is
multiline or not. If it is not specified, the value will be considered
as multiline if it contains a newline character. The value will then be split
into multiple lines. If the value does not contain a newline character, it
will be considered as a single line value. The indent parameter can be used
to specify the indentation of the multiline value. Indentations are with spaces.
:param section: The section to set the option in
:param option: The option to set
:param value: The value to set
:param multiline: Whether the value is multiline or not
:param indent: The indentation for multiline values
"""
if section not in self._all_sections:
raise NoSectionError(section)
# prepare the options value and raw value depending on the multiline flag
_raw_value: List[str] | None = None
if multiline or "\n" in value:
_multiline = True
_raw: str = f"{option}:\n"
_value: List[str] = value.split("\n")
_raw_value: List[str] = [f"{' ' * indent}{v}\n" for v in _value]
else:
_multiline = False
_raw: str = f"{option}: {value}\n"
_value: str = value
# the option does not exist yet
if option not in self._all_options.get(section):
_option: Option = {
"is_multiline": _multiline,
"option": option,
"value": _value,
"_raw": _raw,
}
if _raw_value is not None:
_option["_raw_value"] = _raw_value
self._config[section]["body"].insert(0, _option)
# the option exists and we need to update it
else:
for _option in self._config[section]["body"]:
if _option["option"] == option:
if multiline:
_option["_raw"] = _raw
else:
# we preserve inline comments by replacing the old value with the new one
_option["_raw"] = _option["_raw"].replace(
_option["value"], _value
)
_option["value"] = _value
if _raw_value is not None:
_option["_raw_value"] = _raw_value
break
self._all_options[section][option] = _value
def remove_option(self, section: str, option: str) -> None:
"""Remove the given option from the given section"""
if section not in self._all_sections:
raise NoSectionError(section)
if option not in self._all_options.get(section):
raise NoOptionError(option, section)
for _option in self._config[section]["body"]:
if _option["option"] == option:
del self._all_options[section][option]
self._config[section]["body"].remove(_option)
break
def has_section(self, section: str) -> bool:
"""Return True if the given section exists, False otherwise"""
return section in self._all_sections
def has_option(self, section: str, option: str) -> bool:
"""Return True if the given option exists in the given section, False otherwise"""
return option in self._all_options.get(section)
def _is_section(self, line: str) -> bool:
"""Check if the given line contains a section definition"""
return self._SECTION_RE.match(line) is not None
def _is_option(self, line: str) -> bool:
"""Check if the given line contains an option definition"""
match: Match[str] | None = self._OPTION_RE.match(line)
if not match:
return False
# if there is no value, it's not a regular option but a multiline option
if match.group(2).strip() == "":
return False
if not match.group(1).strip() == "":
return True
return False
def _is_comment(self, line: str) -> bool:
"""Check if the given line is a comment"""
return self._COMMENT_RE.match(line) is not None
def _is_empty_line(self, line: str) -> bool:
"""Check if the given line is an empty line"""
return self._EMPTY_LINE_RE.match(line) is not None
def _is_multiline_option(self, line: str) -> bool:
"""Check if the given line starts a multiline option block"""
match: Match[str] | None = self._MLOPTION_RE.match(line)
if not match:
return False
return True
def _parse_config(self, content: List[str]) -> None:
"""Parse the given content and store the result in the internal state"""
_curr_multi_opt = ""
# THE ORDER MATTERS, DO NOT REORDER THE CONDITIONS!
for line in content:
if self._is_section(line):
self._parse_section(line)
elif self._is_option(line):
self._parse_option(line)
# if it's not a regular option with the value inline,
# it might be a might be a multiline option block
elif self._is_multiline_option(line):
self.in_option_block = True
_curr_multi_opt = self._OPTION_RE.match(line).group(1).strip()
self._add_option_to_section_body(_curr_multi_opt, "", line)
elif self.in_option_block:
self._parse_multiline_option(_curr_multi_opt, line)
# if it's nothing from above, it's probably a comment or an empty line
elif self._is_comment(line) or self._is_empty_line(line):
self._parse_comment(line)
def _parse_section(self, line: str) -> None:
"""Parse a section line and store the result in the internal state"""
match: Match[str] | None = self._SECTION_RE.match(line)
if not match:
return
self.in_option_block = False
section_name: str = match.group(1).strip()
self._store_internal_state_section(section_name, line)
def _store_internal_state_section(self, section: str, raw_value: str) -> None:
"""Store the given section and its raw value in the internal state"""
if section in self._all_sections:
raise DuplicateSectionError(section)
self.section_name = section
self._all_sections.append(section)
self._all_options[section] = {}
self._config[section]: Section = {"_raw": raw_value, "body": []}
def _parse_option(self, line: str) -> None:
"""Parse an option line and store the result in the internal state"""
self.in_option_block = False
match: Match[str] | None = self._OPTION_RE.match(line)
if not match:
return
option: str = match.group(1).strip()
value: str = match.group(2).strip()
if ";" in value:
i = value.index(";")
value = value[:i].strip()
elif "#" in value:
i = value.index("#")
value = value[:i].strip()
self._store_internal_state_option(option, value, line)
def _store_internal_state_option(
self, option: str, value: str, raw_value: str
) -> None:
"""Store the given option and its raw value in the internal state"""
section_options = self._all_options.setdefault(self.section_name, {})
if option in section_options:
raise DuplicateOptionError(option, self.section_name)
section_options[option] = value
self._add_option_to_section_body(option, value, raw_value)
def _parse_multiline_option(self, curr_ml_opt: str, line: str) -> None:
"""Parse a multiline option line and store the result in the internal state"""
section_options = self._all_options.setdefault(self.section_name, {})
multiline_options = section_options.setdefault(curr_ml_opt, [])
_cleaned_line = line.strip().strip("\n")
if _cleaned_line and not self._is_comment(line):
multiline_options.append(_cleaned_line)
# add the option to the internal multiline option value state
self._ensure_section_body_exists()
for _option in self._config[self.section_name]["body"]:
if _option.get("option") == curr_ml_opt:
_option.update(
is_multiline=True,
_raw_value=_option.get("_raw_value", []) + [line],
value=multiline_options,
)
def _parse_comment(self, line: str) -> None:
"""
Parse a comment line and store the result in the internal state
If the there was no previous section parsed, the lines are handled as
the file header and added to the internal header list as it means, that
we are at the very top of the file.
"""
self.in_option_block = False
if not self.section_name:
self._header.append(line)
else:
self._add_option_to_section_body("", "", line)
def _ensure_section_body_exists(self) -> None:
"""
Ensure that the section body exists in the internal state.
If the section body does not exist, it is created as an empty list
"""
if self.section_name not in self._config:
self._config.setdefault(self.section_name, {}).setdefault("body", [])
def _add_option_to_section_body(
self, option: str, value: str, line: str, is_multiline: bool = False
) -> None:
"""Add a raw option line to the internal state"""
self._ensure_section_body_exists()
new_option: Option = {
"is_multiline": is_multiline,
"option": option,
"value": value,
"_raw": line,
}
option_body = self._config[self.section_name]["body"]
option_body.append(new_option)
raise ValueError(
f"Cannot convert {self.getval(section, option)} to {conv.__name__}"
) from e
def _generate_rand_id(self) -> str:
"""Generate a random id with 6 characters"""
chars = string.ascii_letters + string.digits
rand_string = "".join(secrets.choice(chars) for _ in range(12))
return f"#_{rand_string}"

View File

@@ -1,21 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Test SimpleConfigParser" type="tests" factoryName="py.test">
<module name="simple-config-parser" />
<option name="ENV_FILES" value="" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" />
<option name="SDK_HOME" value="" />
<option name="SDK_NAME" value="Python 3.8 (simple-config-parser)" />
<option name="WORKING_DIRECTORY" value="" />
<option name="IS_MODULE_SDK" value="false" />
<option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" />
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
<option name="_new_keywords" value="&quot;&quot;" />
<option name="_new_parameters" value="&quot;&quot;" />
<option name="_new_additionalArguments" value="&quot;-s -vv&quot;" />
<option name="_new_target" value="&quot;&quot;" />
<option name="_new_targetType" value="&quot;PATH&quot;" />
<method v="2" />
</configuration>
</component>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,32 @@
# a comment at the very top
# should be treated as the file header
# up to the first section, including all blank lines
[section_1]
option_1: value_1
option_1_1: True # this is a boolean
option_1_2: 5 ; this is an integer
option_1_3: 1.123 #;this is a float
[section_2] ; comment
option_2: value_2
; comment
[section_3]
option_3: value_3 # comment
[section_4]
# comment
option_4: value_4
[section number 5]
#option_5: value_5
option_5 = this.is.value-5
multi_option:
# these are multi-line values
value_5_1
value_5_2 ; here is a comment
value_5_3
option_5_1: value_5_1

View File

@@ -1,95 +0,0 @@
import pytest
from src.simple_config_parser.simple_config_parser import SimpleConfigParser
@pytest.fixture
def parser():
parser = SimpleConfigParser()
parser._header = ["header1\n", "header2\n"]
parser._config = {
"section1": {
"_raw": "[section1]\n",
"body": [
{
"_raw": "option1: value1\n",
"_raw_value": "value1\n",
"is_multiline": False,
"option": "option1",
"value": "value1",
},
{
"_raw": "option2: value2\n",
"_raw_value": "value2\n",
"is_multiline": False,
"option": "option2",
"value": "value2",
},
],
},
"section2": {
"_raw": "[section2]\n",
"body": [
{
"_raw": "option3: value3\n",
"_raw_value": "value3\n",
"is_multiline": False,
"option": "option3",
"value": "value3",
},
],
},
"section3": {
"_raw": "[section3]\n",
"body": [
{
"_raw": "option4:\n",
"_raw_value": [" value4\n", " value5\n", " value6\n"],
"is_multiline": True,
"option": "option4",
"value": ["value4", "value5", "value6"],
},
],
},
}
return parser
def test_construct_content(parser):
content = parser._construct_content()
assert (
content == "header1\nheader2\n"
"[section1]\n"
"option1: value1\n"
"option2: value2\n"
"[section2]\n"
"option3: value3\n"
"[section3]\n"
"option4:\n"
" value4\n"
" value5\n"
" value6\n"
)
def test_construct_content_no_header(parser):
parser._header = None
content = parser._construct_content()
assert (
content == "[section1]\n"
"option1: value1\n"
"option2: value2\n"
"[section2]\n"
"option3: value3\n"
"[section3]\n"
"option4:\n"
" value4\n"
" value5\n"
" value6\n"
)
def test_construct_content_no_sections(parser):
parser._config = {}
content = parser._construct_content()
assert content == "".join(parser._header)

View File

@@ -1,84 +0,0 @@
import pytest
from src.simple_config_parser.simple_config_parser import (
DuplicateOptionError,
DuplicateSectionError,
SimpleConfigParser,
)
@pytest.fixture
def parser():
return SimpleConfigParser()
class TestInternalStateChanges:
@pytest.mark.parametrize(
"given", ["dummy_section", "dummy_section 2", "another_section"]
)
def test_ensure_section_body_exists(self, parser, given):
parser._config = {}
parser.section_name = given
parser._ensure_section_body_exists()
assert parser._config[given] is not None
assert parser._config[given]["body"] == []
def test_add_option_to_section_body(self):
pass
@pytest.mark.parametrize(
"given", ["dummy_section", "dummy_section 2", "another_section\n"]
)
def test_store_internal_state_section(self, parser, given):
parser._store_internal_state_section(given, given)
assert parser._all_sections == [given]
assert parser._all_options[given] == {}
assert parser._config[given]["body"] == []
assert parser._config[given]["_raw"] == given
def test_duplicate_section_error(self, parser):
section_name = "dummy_section"
parser._all_sections = [section_name]
with pytest.raises(DuplicateSectionError) as excinfo:
parser._store_internal_state_section(section_name, section_name)
message = f"Section '{section_name}' is defined more than once"
assert message in str(excinfo.value)
# Check that the internal state of the parser is correct
assert parser.in_option_block is False
assert parser.section_name == ""
assert parser._all_sections == [section_name]
@pytest.mark.parametrize(
"given_name, given_value, given_raw_value",
[("dummyoption", "dummyvalue", "dummyvalue\n")],
)
def test_store_internal_state_option(
self, parser, given_name, given_value, given_raw_value
):
parser.section_name = "dummy_section"
parser._store_internal_state_option(given_name, given_value, given_raw_value)
assert parser._all_options[parser.section_name] == {given_name: given_value}
new_option = {
"is_multiline": False,
"option": given_name,
"value": given_value,
"_raw": given_raw_value,
}
assert parser._config[parser.section_name]["body"] == [new_option]
def test_duplicate_option_error(self, parser):
option_name = "dummyoption"
value = "dummyvalue"
parser.section_name = "dummy_section"
parser._all_options = {parser.section_name: {option_name: value}}
with pytest.raises(DuplicateOptionError) as excinfo:
parser._store_internal_state_option(option_name, value, value)
message = f"Option '{option_name}' in section '{parser.section_name}' is defined more than once"
assert message in str(excinfo.value)

View File

@@ -1,6 +0,0 @@
testcases = [
"# comment # 1",
"; comment # 2",
" ; indented comment",
" # another indented comment",
]

View File

@@ -1,28 +0,0 @@
testcases = [
("option: value", "option", "value"),
("option : value", "option", "value"),
("option :value", "option", "value"),
("option= value", "option", "value"),
("option = value", "option", "value"),
("option =value", "option", "value"),
("option: value\n", "option", "value"),
("option: value # inline comment", "option", "value"),
("option: value # inline comment\n", "option", "value"),
(
"description: Helper: park toolhead used in PAUSE and CANCEL_PRINT",
"description",
"Helper: park toolhead used in PAUSE and CANCEL_PRINT",
),
("description: homing!", "description", "homing!"),
("description: inline macro :-)", "description", "inline macro :-)"),
("path: %GCODES_DIR%", "path", "%GCODES_DIR%"),
(
"serial = /dev/serial/by-id/<your-mcu-id>",
"serial",
"/dev/serial/by-id/<your-mcu-id>",
),
("parameter_temperature_(°C): 155", "parameter_temperature_(°C)", "155"),
("parameter_humidity_(%_RH): 45", "parameter_humidity_(%_RH)", "45"),
("parameter_spool_weight_(%): 10", "parameter_spool_weight_(%)", "10"),
("path: /dev/shm/drying_box.json", "path", "/dev/shm/drying_box.json"),
]

View File

@@ -1,8 +0,0 @@
testcases = [
("[test_section]", "test_section"),
("[test_section two]", "test_section two"),
("[section1] # inline comment", "section1"),
("[section2] ; second comment", "section2"),
("[include moonraker-obico-update.cfg]", "include moonraker-obico-update.cfg"),
("[include moonraker_obico_macros.cfg]", "include moonraker_obico_macros.cfg"),
]

View File

@@ -1,92 +0,0 @@
import pytest
from data.case_parse_comment import testcases as case_parse_comment
from data.case_parse_option import testcases as case_parse_option
from data.case_parse_section import testcases as case_parse_section
from src.simple_config_parser.simple_config_parser import (
Option,
SimpleConfigParser,
)
@pytest.fixture
def parser():
return SimpleConfigParser()
class TestSingleLineParsing:
@pytest.mark.parametrize("given, expected", [*case_parse_section])
def test_parse_section(self, parser, given, expected):
parser._parse_section(given)
# Check that the internal state of the parser is correct
assert parser.section_name == expected
assert parser.in_option_block is False
assert parser._all_sections == [expected]
assert parser._config[expected]["_raw"] == given
assert parser._config[expected]["body"] == []
@pytest.mark.parametrize(
"given, expected_option, expected_value", [*case_parse_option]
)
def test_parse_option(self, parser, given, expected_option, expected_value):
section_name = "test_section"
parser.section_name = section_name
parser._parse_option(given)
# Check that the internal state of the parser is correct
assert parser.section_name == section_name
assert parser.in_option_block is False
assert parser._all_options[section_name][expected_option] == expected_value
section_option = parser._config[section_name]["body"][0]
assert section_option["option"] == expected_option
assert section_option["value"] == expected_value
assert section_option["_raw"] == given
@pytest.mark.parametrize(
"option, next_line",
[("gcode", "next line"), ("gcode", " {{% some jinja template %}}")],
)
def test_parse_multiline_option(self, parser, option, next_line):
parser.section_name = "dummy_section"
parser.in_option_block = True
parser._add_option_to_section_body(option, "", option)
parser._parse_multiline_option(option, next_line)
cleaned_next_line = next_line.strip().strip("\n")
assert parser._all_options[parser.section_name] is not None
assert parser._all_options[parser.section_name][option] == [cleaned_next_line]
expected_option: Option = {
"is_multiline": True,
"option": option,
"value": [cleaned_next_line],
"_raw": option,
"_raw_value": [next_line],
}
assert parser._config[parser.section_name]["body"] == [expected_option]
@pytest.mark.parametrize("given", [*case_parse_comment])
def test_parse_comment(self, parser, given):
parser.section_name = "dummy_section"
parser._parse_comment(given)
# internal state checks after parsing
assert parser.in_option_block is False
expected_option = {
"is_multiline": False,
"_raw": given,
"option": "",
"value": "",
}
assert parser._config[parser.section_name]["body"] == [expected_option]
@pytest.mark.parametrize("given", ["# header line", "; another header line"])
def test_parse_header_comment(self, parser, given):
parser.section_name = ""
parser._parse_comment(given)
assert parser.in_option_block is False
assert parser._header == [given]

View File

@@ -1,9 +0,0 @@
testcases = [
("# an arbitrary comment", True),
("; another arbitrary comment", True),
(" ; indented comment", True),
(" # indented comment", True),
("not_a: comment", False),
("also_not_a= comment", False),
("[definitely_not_a_comment]", False),
]

View File

@@ -1,9 +0,0 @@
testcases = [
("", True),
(" ", True),
("not empty", False),
(" # indented comment", False),
("not: empty", False),
("also_not= empty", False),
("[definitely_not_empty]", False),
]

View File

@@ -1,27 +0,0 @@
testcases = [
("valid_option:", True),
("valid_option:\n", True),
("valid_option: ; inline comment", True),
("valid_option: # inline comment", True),
("valid_option :", True),
("valid_option=", True),
("valid_option= ", True),
("valid_option =", True),
("valid_option = ", True),
("invalid_option ==", False),
("invalid_option :=", False),
("not_a_valid_option", False),
("", False),
("# that's a comment", False),
("; that's a comment", False),
("parameter_humidity_(%_RH):", True),
("parameter_spool_weight_(%):", True),
("parameter_temperature_(°C):", True),
("parameter_humidity_(%_RH): 18.123", False),
("parameter_spool_weight_(%): 150", False),
("parameter_temperature_(°C): 30,5", False),
("trusted_clients:", True),
("trusted_clients: 192.168.1.0/24", False),
("cors_domains:", True),
("cors_domains: http://*.lan", False),
]

View File

@@ -1,31 +0,0 @@
testcases = [
("valid_option: value", True),
("valid_option: value\n", True),
("valid_option: value ; inline comment", True),
("valid_option: value # inline comment", True),
("valid_option: value # inline comment\n", True),
("valid_option : value", True),
("valid_option :value", True),
("valid_option= value", True),
("valid_option = value", True),
("valid_option =value", True),
("invalid_option:", False),
("invalid_option=", False),
("invalid_option:: value", False),
("invalid_option :: value", False),
("invalid_option ::value", False),
("invalid_option== value", False),
("invalid_option == value", False),
("invalid_option ==value", False),
("invalid_option:= value", False),
("invalid_option := value", False),
("invalid_option :=value", False),
("[that_is_a_section]", False),
("[that_is_section two]", False),
("not_a_valid_option", False),
("description: homing!", True),
("description: inline macro :-)", True),
("path: %GCODES_DIR%", True),
("path: /dev/shm/drying_box.json", True),
("serial = /dev/serial/by-id/<your-mcu-id>", True),
]

View File

@@ -1,12 +0,0 @@
testcases = [
("[example_section]", True),
("[gcode_macro CANCEL_PRINT]", True),
("[gcode_macro SET_PAUSE_NEXT_LAYER]", True),
("[gcode_macro _TOOLHEAD_PARK_PAUSE_CANCEL]", True),
("[update_manager moonraker-obico]", True),
("[include moonraker_obico_macros.cfg]", True),
("[include moonraker-obico-update.cfg]", True),
("[example_section two]", True),
("not_a_valid_section", False),
("section: invalid", False),
]

View File

@@ -1,37 +0,0 @@
import pytest
from data.case_line_is_comment import testcases as case_line_is_comment
from data.case_line_is_empty import testcases as case_line_is_empty
from data.case_line_is_multiline_option import (
testcases as case_line_is_multiline_option,
)
from data.case_line_is_option import testcases as case_line_is_option
from data.case_line_is_section import testcases as case_line_is_section
from src.simple_config_parser.simple_config_parser import SimpleConfigParser
@pytest.fixture
def parser():
return SimpleConfigParser()
class TestLineTypeDetection:
@pytest.mark.parametrize("given, expected", [*case_line_is_section])
def test_line_is_section(self, parser, given, expected):
assert parser._is_section(given) is expected
@pytest.mark.parametrize("given, expected", [*case_line_is_option])
def test_line_is_option(self, parser, given, expected):
assert parser._is_option(given) is expected
@pytest.mark.parametrize("given, expected", [*case_line_is_multiline_option])
def test_line_is_multiline_option(self, parser, given, expected):
assert parser._is_multiline_option(given) is expected
@pytest.mark.parametrize("given, expected", [*case_line_is_comment])
def test_line_is_comment(self, parser, given, expected):
assert parser._is_comment(given) is expected
@pytest.mark.parametrize("given, expected", [*case_line_is_empty])
def test_line_is_empty(self, parser, given, expected):
assert parser._is_empty_line(given) is expected

View File

@@ -1,196 +0,0 @@
import pytest
from src.simple_config_parser.simple_config_parser import (
DuplicateSectionError,
NoOptionError,
NoSectionError,
SimpleConfigParser,
)
@pytest.fixture
def parser():
return SimpleConfigParser()
class TestPublicAPI:
def test_has_section(self, parser):
parser._all_sections = ["section1"]
assert parser.has_section("section1") is True
@pytest.mark.parametrize("section", ["section1", "section2", "section three"])
def test_add_section(self, parser, section):
parser.add_section(section)
assert section in parser._all_sections
assert parser._all_options[section] == {}
cfg_section = {"_raw": f"\n[{section}]\n", "body": []}
assert parser._config[section] == cfg_section
@pytest.mark.parametrize("section", ["section1", "section2", "section three"])
def test_add_existing_section(self, parser, section):
parser._all_sections = [section]
with pytest.raises(DuplicateSectionError):
parser.add_section(section)
assert parser._all_sections == [section]
@pytest.mark.parametrize("section", ["section1", "section2", "section three"])
def test_remove_section(self, parser, section):
parser.add_section(section)
parser.remove_section(section)
assert section not in parser._all_sections
assert section not in parser._all_options
assert section not in parser._config
@pytest.mark.parametrize("section", ["section1", "section2", "section three"])
def test_remove_non_existing_section(self, parser, section):
with pytest.raises(NoSectionError):
parser.remove_section(section)
def test_get_all_sections(self, parser):
parser.add_section("section1")
parser.add_section("section2")
parser.add_section("section three")
assert parser.sections() == ["section1", "section2", "section three"]
def test_has_option(self, parser):
parser.add_section("section1")
parser.set("section1", "option1", "value1")
assert parser.has_option("section1", "option1") is True
@pytest.mark.parametrize(
"section, option, value",
[
("section1", "option1", "value1"),
("section2", "option2", "value2"),
("section three", "option3", "value three"),
],
)
def test_set_new_option(self, parser, section, option, value):
parser.add_section(section)
parser.set(section, option, value)
assert section in parser._all_sections
assert option in parser._all_options[section]
assert parser._all_options[section][option] == value
assert parser._config[section]["body"][0]["is_multiline"] is False
assert parser._config[section]["body"][0]["option"] == option
assert parser._config[section]["body"][0]["value"] == value
assert parser._config[section]["body"][0]["_raw"] == f"{option}: {value}\n"
def test_set_existing_option(self, parser):
section, option, value1, value2 = "section1", "option1", "value1", "value2"
parser.add_section(section)
parser.set(section, option, value1)
parser.set(section, option, value2)
assert parser._all_options[section][option] == value2
assert parser._config[section]["body"][0]["is_multiline"] is False
assert parser._config[section]["body"][0]["option"] == option
assert parser._config[section]["body"][0]["value"] == value2
assert parser._config[section]["body"][0]["_raw"] == f"{option}: {value2}\n"
def test_set_new_multiline_option(self, parser):
section, option, value = "section1", "option1", "value1\nvalue2\nvalue3"
parser.add_section(section)
parser.set(section, option, value)
assert parser._config[section]["body"][0]["is_multiline"] is True
assert parser._config[section]["body"][0]["option"] == option
values = ["value1", "value2", "value3"]
raw_values = [" value1\n", " value2\n", " value3\n"]
assert parser._config[section]["body"][0]["value"] == values
assert parser._config[section]["body"][0]["_raw"] == f"{option}:\n"
assert parser._config[section]["body"][0]["_raw_value"] == raw_values
assert parser._all_options[section][option] == values
def test_set_option_of_non_existing_section(self, parser):
with pytest.raises(NoSectionError):
parser.set("section1", "option1", "value1")
def test_remove_option(self, parser):
section, option, value = "section1", "option1", "value1"
parser.add_section(section)
parser.set(section, option, value)
parser.remove_option(section, option)
assert option not in parser._all_options[section]
assert option not in parser._config[section]["body"]
def test_remove_non_existing_option(self, parser):
parser.add_section("section1")
with pytest.raises(NoOptionError):
parser.remove_option("section1", "option1")
def test_remove_option_of_non_existing_section(self, parser):
with pytest.raises(NoSectionError):
parser.remove_option("section1", "option1")
def test_get_option(self, parser):
parser.add_section("section1")
parser.add_section("section2")
parser.set("section1", "option1", "value1")
parser.set("section2", "option2", "value2")
parser.set("section2", "option3", "value two")
assert parser.get("section1", "option1") == "value1"
assert parser.get("section2", "option2") == "value2"
assert parser.get("section2", "option3") == "value two"
def test_get_option_of_non_existing_section(self, parser):
with pytest.raises(NoSectionError):
parser.get("section1", "option1")
def test_get_option_of_non_existing_option(self, parser):
parser.add_section("section1")
with pytest.raises(NoOptionError):
parser.get("section1", "option1")
def test_get_option_fallback(self, parser):
parser.add_section("section1")
assert parser.get("section1", "option1", "fallback_value") == "fallback_value"
def test_get_options(self, parser):
parser.add_section("section1")
parser.set("section1", "option1", "value1")
parser.set("section1", "option2", "value2")
parser.set("section1", "option3", "value3")
options = {"option1": "value1", "option2": "value2", "option3": "value3"}
assert parser.options("section1") == options
def test_get_option_as_int(self, parser):
parser.add_section("section1")
parser.set("section1", "option1", "1")
option = parser.getint("section1", "option1")
assert isinstance(option, int) is True
def test_get_option_as_float(self, parser):
parser.add_section("section1")
parser.set("section1", "option1", "1.234")
option = parser.getfloat("section1", "option1")
assert isinstance(option, float) is True
@pytest.mark.parametrize(
"value",
["True", "true", "on", "1", "yes", "False", "false", "off", "0", "no"],
)
def test_get_option_as_boolean(self, parser, value):
parser.add_section("section1")
parser.set("section1", "option1", value)
option = parser.getboolean("section1", "option1")
assert isinstance(option, bool) is True

View File

@@ -0,0 +1,7 @@
not_empty
[also_not_empty]
#
;
;
#
option: value

View File

@@ -0,0 +1,39 @@
# ======================================================================= #
# Copyright (C) 2024 Dominik Willner <th33xitus@gmail.com> #
# #
# https://github.com/dw-0/simple-config-parser #
# #
# This file may be distributed under the terms of the GNU GPLv3 license #
# ======================================================================= #
from pathlib import Path
import pytest
from src.simple_config_parser.simple_config_parser import SimpleConfigParser
from tests.utils import load_testdata_from_file
BASE_DIR = Path(__file__).parent.joinpath("test_data")
MATCHING_TEST_DATA_PATH = BASE_DIR.joinpath("matching_data.txt")
NON_MATCHING_TEST_DATA_PATH = BASE_DIR.joinpath("non_matching_data.txt")
@pytest.fixture
def parser():
return SimpleConfigParser()
@pytest.mark.parametrize("line", load_testdata_from_file(MATCHING_TEST_DATA_PATH))
def test_match_line_comment(parser, line):
"""Test that a line matches the definition of a line comment"""
assert (
parser._match_empty_line(line) is True
), f"Expected line '{line}' to match line comment definition!"
@pytest.mark.parametrize("line", load_testdata_from_file(NON_MATCHING_TEST_DATA_PATH))
def test_non_matching_line_comment(parser, line):
"""Test that a line does not match the definition of a line comment"""
assert (
parser._match_empty_line(line) is False
), f"Expected line '{line}' to not match line comment definition!"

View File

@@ -0,0 +1,28 @@
;[example_section]
#[example_section]
# [example_section]
; [example_section]
;[gcode_macro CANCEL_PRINT]
#[gcode_macro CANCEL_PRINT]
# [gcode_macro CANCEL_PRINT]
; [gcode_macro CANCEL_PRINT]
;[gcode_macro SET_PAUSE_NEXT_LAYER]
#[gcode_macro SET_PAUSE_NEXT_LAYER]
# [gcode_macro SET_PAUSE_NEXT_LAYER]
; [gcode_macro SET_PAUSE_NEXT_LAYER]
;[gcode_macro _TOOLHEAD_PARK_PAUSE_CANCEL]
#[gcode_macro _TOOLHEAD_PARK_PAUSE_CANCEL]
# [gcode_macro _TOOLHEAD_PARK_PAUSE_CANCEL]
; [gcode_macro _TOOLHEAD_PARK_PAUSE_CANCEL]
;[gcode_macro _TOOLHEAD_PARK_PAUSE_CANCEL]
#[gcode_macro _TOOLHEAD_PARK_PAUSE_CANCEL]
# [gcode_macro _TOOLHEAD_PARK_PAUSE_CANCEL]
; [gcode_macro _TOOLHEAD_PARK_PAUSE_CANCEL]
;[gcode_macro _TOOLHEAD_PARK_PAUSE_CANCEL]
#[gcode_macro _TOOLHEAD_PARK_PAUSE_CANCEL]
# [gcode_macro _TOOLHEAD_PARK_PAUSE_CANCEL]
; [gcode_macro _TOOLHEAD_PARK_PAUSE_CANCEL]
;[gcode_macro _TOOLHEAD_PARK_PAUSE_CANCEL]
#[gcode_macro _TOOLHEAD_PARK_PAUSE_CANCEL]
# [gcode_macro _TOOLHEAD_PARK_PAUSE_CANCEL]
; [gcode_macro _TOOLHEAD_PARK_PAUSE_CANCEL]

View File

@@ -0,0 +1,5 @@
not_a_comment: nono
[also not a comment]
not_a_comment: ; comment
not_a_comment: # comment

View File

@@ -0,0 +1,39 @@
# ======================================================================= #
# Copyright (C) 2024 Dominik Willner <th33xitus@gmail.com> #
# #
# https://github.com/dw-0/simple-config-parser #
# #
# This file may be distributed under the terms of the GNU GPLv3 license #
# ======================================================================= #
from pathlib import Path
import pytest
from src.simple_config_parser.simple_config_parser import SimpleConfigParser
from tests.utils import load_testdata_from_file
BASE_DIR = Path(__file__).parent.joinpath("test_data")
MATCHING_TEST_DATA_PATH = BASE_DIR.joinpath("matching_data.txt")
NON_MATCHING_TEST_DATA_PATH = BASE_DIR.joinpath("non_matching_data.txt")
@pytest.fixture
def parser():
return SimpleConfigParser()
@pytest.mark.parametrize("line", load_testdata_from_file(MATCHING_TEST_DATA_PATH))
def test_match_line_comment(parser, line):
"""Test that a line matches the definition of a line comment"""
assert (
parser._match_line_comment(line) is True
), f"Expected line '{line}' to match line comment definition!"
@pytest.mark.parametrize("line", load_testdata_from_file(NON_MATCHING_TEST_DATA_PATH))
def test_non_matching_line_comment(parser, line):
"""Test that a line does not match the definition of a line comment"""
assert (
parser._match_line_comment(line) is False
), f"Expected line '{line}' to not match line comment definition!"

View File

@@ -0,0 +1,461 @@
baud: 250000
minimum_cruise_ratio: 0.5
square_corner_velocity: 5.0
full_steps_per_rotation: 200
position_min: 0
homing_speed: 5.0
homing_retract_dist: 5.0
kinematics: cartesian
kinematics: delta
minimum_z_position: 0
speed: 50
horizontal_move_z: 5
kinematics: deltesian
minimum_z_position: 0
min_angle: 5
slow_ratio: 3
kinematics: corexy
kinematics: corexz
kinematics: hybrid_corexy
kinematics: hybrid_corexz
kinematics: polar
kinematics: rotary_delta
minimum_z_position: 0
speed: 50
horizontal_move_z: 5
kinematics: winch
kinematics: none
max_velocity: 1
max_accel: 1
instantaneous_corner_velocity: 1.000
max_extrude_only_distance: 50.0
pressure_advance: 0.0
pressure_advance_smooth_time: 0.040
max_power: 1.0
pullup_resistor: 4700
smooth_time: 1.0
max_delta: 2.0
pwm_cycle_time: 0.100
min_extrude_temp: 170
speed: 50
horizontal_move_z: 5
probe_count: 3, 3
round_probe_count: 5
fade_start: 1.0
fade_end: 0.0
split_delta_z: .025
move_check_distance: 5.0
mesh_pps: 2, 2
algorithm: lagrange
bicubic_tension: .2
x_adjust: 0
y_adjust: 0
z_adjust: 0
speed: 50
horizontal_move_z: 5
horizontal_move_z: 5
probe_height: 0
speed: 50
probe_speed: 5
speed: 50
horizontal_move_z: 5
screw_thread: CW-M3
speed: 50
horizontal_move_z: 5
retries: 0
retry_tolerance: 0
speed: 50
horizontal_move_z: 5
max_adjust: 4
retries: 0
retry_tolerance: 0
speed: 50.0
z_hop_speed: 15.0
move_to_previous: False
axes: xyz
endstop_align_zero: False
description: G-Code macro
initial_duration: 0.0
timeout: 600
enable_force_move: False
recover_velocity: 50.
retract_length: 0
retract_speed: 20
unretract_extra_length: 0
unretract_speed: 10
resolution: 1.0
default_type: echo
default_prefix: echo:
shaper_freq_x: 0
shaper_freq_y: 0
shaper_type: mzv
damping_ratio_x: 0.1
damping_ratio_y: 0.1
spi_speed: 5000000
axes_map: x, y, z
rate: 3200
spi_speed: 5000000
axes_map: x, y, z
i2c_speed: 400000
axes_map: x, y, z
min_freq: 5
max_freq: 133.33
accel_per_hz: 75
hz_per_sec: 1
mcu: mcu
deactivate_on_each_sample: True
x_offset: 0.0
y_offset: 0.0
speed: 5.0
samples: 1
sampleretract_dist: 2.0
samples_result: average
samples_tolerance: 0.100
samples_toleranceretries: 0
pin_move_time: 0.680
stow_on_each_sample: True
probe_with_touch_mode: False
pin_up_reports_not_triggered: True
pin_up_touch_modereports_triggered: True
recovery_time: 0.4
sensor_type: ldc1612
speed: 50
horizontal_move_z: 5
calibrate_start_x: 20
calibrate_end_x: 200
calibrate_y: 112.5
max_error: 120
hysteresis: 5
heating_gain: 2
extruder_heating_z: 50.
max_validation_temp: 60.
pullup_resistor: 4700
inlineresistor: 0
adc_voltage: 5.0
voltage_offset: 0
sensor_type: PT1000
pullup_resistor: 4700
spi_speed: 4000000
tc_type: K
tc_use_50Hz_filter: False
tc_averaging_count: 1
rtd_nominal_r: 100
rtd_referencer: 430
rtd_num_of_wires: 2
rtd_use_50Hz_filter: False
sensor_type: BME280
sensor_type: AHT10
sensor_type: temperature_mcu
sensor_mcu: mcu
sensor_type: temperature_host
sensor_type: DS18B20
sensor_type: temperature_combined
max_power: 1.0
shutdown_speed: 0
cycle_time: 0.010
hardware_pwm: False
kick_start_time: 0.100
off_below: 0.0
tachometer_ppr: 2
tachometer_poll_interval: 0.0015
heater: extruder
heater_temp: 50.0
fan_speed: 1.0
fan_speed: 1.0
pid_deriv_time: 2.0
target_temp: 40.0
max_speed: 1.0
min_speed: 0.3
cycle_time: 0.010
hardware_pwm: False
initial_RED: 0.0
initial_GREEN: 0.0
initial_BLUE: 0.0
initial_WHITE: 0.0
color_order: GRB
initial_RED: 0.0
initial_GREEN: 0.0
initial_BLUE: 0.0
initial_WHITE: 0.0
initial_RED: 0.0
initial_GREEN: 0.0
initial_BLUE: 0.0
i2c_address: 98
initial_RED: 0.0
initial_GREEN: 0.0
initial_BLUE: 0.0
initial_WHITE: 0.0
i2c_address: 98
color_order: RGBW
initial_RED: 0.0
initial_GREEN: 0.0
initial_BLUE: 0.0
initial_WHITE: 0.0
maximum_servo_angle: 180
minimum_pulse_width: 0.001
maximum_pulse_width: 0.002
pwm: False
cycle_time: 0.100
hardware_pwm: False
cycle_time: 0.100
hardware_pwm: False
cycle_time: 0.100
interpolate: True
senseresistor: 0.110
stealthchop_threshold: 0
driver_MSLUT0: 2863314260
driver_MSLUT1: 1251300522
driver_MSLUT2: 608774441
driver_MSLUT3: 269500962
driver_MSLUT4: 4227858431
driver_MSLUT5: 3048961917
driver_MSLUT6: 1227445590
driver_MSLUT7: 4211234
driver_W0: 2
driver_W1: 1
driver_W2: 1
driver_W3: 1
driver_X1: 128
driver_X2: 255
driver_X3: 255
driver_START_SIN: 0
driver_START_SIN90: 247
driver_IHOLDDELAY: 8
driver_TPOWERDOWN: 0
driver_TBL: 1
driver_TOFF: 4
driver_HEND: 7
driver_HSTRT: 0
driver_VHIGHFS: 0
driver_VHIGHCHM: 0
driver_PWM_AUTOSCALE: True
driver_PWM_FREQ: 1
driver_PWM_GRAD: 4
driver_PWM_AMPL: 128
driver_SGT: 0
driver_SEMIN: 0
driver_SEUP: 0
driver_SEMAX: 0
driver_SEDN: 0
driver_SEIMIN: 0
driver_SFILT: 0
interpolate: True
sense_resistor: 0.110
stealthchop_threshold: 0
driver_MULTISTEP_FILT: True
driver_IHOLDDELAY: 8
driver_TPOWERDOWN: 20
driver_TBL: 2
driver_TOFF: 3
driver_HEND: 0
driver_HSTRT: 5
driver_PWM_AUTOGRAD: True
driver_PWM_AUTOSCALE: True
driver_PWM_LIM: 12
driver_PWM_REG: 8
driver_PWM_FREQ: 1
driver_PWM_GRAD: 14
driver_PWM_OFS: 36
interpolate: True
sense_resistor: 0.110
stealthchop_threshold: 0
driver_MULTISTEP_FILT: True
driver_IHOLDDELAY: 8
driver_TPOWERDOWN: 20
driver_TBL: 2
driver_TOFF: 3
driver_HEND: 0
driver_HSTRT: 5
driver_PWM_AUTOGRAD: True
driver_PWM_AUTOSCALE: True
driver_PWM_LIM: 12
driver_PWM_REG: 8
driver_PWM_FREQ: 1
driver_PWM_GRAD: 14
driver_PWM_OFS: 36
driver_SGTHRS: 0
driver_SEMIN: 0
driver_SEUP: 0
driver_SEMAX: 0
driver_SEDN: 0
driver_SEIMIN: 0
spi_speed: 4000000
interpolate: True
idle_current_percent: 100
driver_TBL: 2
driver_RNDTF: 0
driver_HDEC: 0
driver_CHM: 0
driver_HEND: 3
driver_HSTRT: 3
driver_TOFF: 4
driver_SEIMIN: 0
driver_SEDN: 0
driver_SEMAX: 0
driver_SEUP: 0
driver_SEMIN: 0
driver_SFILT: 0
driver_SGT: 0
driver_SLPH: 0
driver_SLPL: 0
driver_DISS2G: 0
driver_TS2G: 3
interpolate: True
rref: 12000
stealthchop_threshold: 0
driver_MSLUT0: 2863314260
driver_MSLUT1: 1251300522
driver_MSLUT2: 608774441
driver_MSLUT3: 269500962
driver_MSLUT4: 4227858431
driver_MSLUT5: 3048961917
driver_MSLUT6: 1227445590
driver_MSLUT7: 4211234
driver_W0: 2
driver_W1: 1
driver_W2: 1
driver_W3: 1
driver_X1: 128
driver_X2: 255
driver_X3: 255
driver_START_SIN: 0
driver_START_SIN90: 247
driver_OFFSET_SIN90: 0
driver_MULTISTEP_FILT: True
driver_IHOLDDELAY: 6
driver_IRUNDELAY: 4
driver_TPOWERDOWN: 10
driver_TBL: 2
driver_TOFF: 3
driver_HEND: 2
driver_HSTRT: 5
driver_FD3: 0
driver_TPFD: 4
driver_CHM: 0
driver_VHIGHFS: 0
driver_VHIGHCHM: 0
driver_DISS2G: 0
driver_DISS2VS: 0
driver_PWM_AUTOSCALE: True
driver_PWM_AUTOGRAD: True
driver_PWM_FREQ: 0
driver_FREEWHEEL: 0
driver_PWM_GRAD: 0
driver_PWM_OFS: 29
driver_PWM_REG: 4
driver_PWM_LIM: 12
driver_SGT: 0
driver_SEMIN: 0
driver_SEUP: 0
driver_SEMAX: 0
driver_SEDN: 0
driver_SEIMIN: 0
driver_SFILT: 0
driver_SG4_ANGLE_OFFSET: 1
interpolate: True
sense_resistor: 0.075
stealthchop_threshold: 0
driver_MSLUT0: 2863314260
driver_MSLUT1: 1251300522
driver_MSLUT2: 608774441
driver_MSLUT3: 269500962
driver_MSLUT4: 4227858431
driver_MSLUT5: 3048961917
driver_MSLUT6: 1227445590
driver_MSLUT7: 4211234
driver_W0: 2
driver_W1: 1
driver_W2: 1
driver_W3: 1
driver_X1: 128
driver_X2: 255
driver_X3: 255
driver_START_SIN: 0
driver_START_SIN90: 247
driver_MULTISTEP_FILT: True
driver_IHOLDDELAY: 6
driver_TPOWERDOWN: 10
driver_TBL: 2
driver_TOFF: 3
driver_HEND: 2
driver_HSTRT: 5
driver_FD3: 0
driver_TPFD: 4
driver_CHM: 0
driver_VHIGHFS: 0
driver_VHIGHCHM: 0
driver_DISS2G: 0
driver_DISS2VS: 0
driver_PWM_AUTOSCALE: True
driver_PWM_AUTOGRAD: True
driver_PWM_FREQ: 0
driver_FREEWHEEL: 0
driver_PWM_GRAD: 0
driver_PWM_OFS: 30
driver_PWM_REG: 4
driver_PWM_LIM: 12
driver_SGT: 0
driver_SEMIN: 0
driver_SEUP: 0
driver_SEMAX: 0
driver_SEDN: 0
driver_SEIMIN: 0
driver_SFILT: 0
driver_DRVSTRENGTH: 0
driver_BBMCLKS: 4
driver_BBMTIME: 0
driver_FILT_ISENSE: 0
i2c_address: 96
analog_pullup_resistor: 4700
lcd_type: hd44780
hd44780_protocol_init: True
lcd_type: hd44780_spi
hd44780_protocol_init: True
lcd_type: st7920
lcd_type: emulated_st7920
lcd_type: uc1701
vcomh: 0
invert: False
x_offset: 0
type: disabled
type: list
type: command
type: input
pause_on_runout: True
event_delay: 3.0
pause_delay: 0.5
detection_length: 7.0
default_nominal_filament_diameter: 1.75
max_difference: 0.2
measurement_delay: 100
cal_dia1: 1.50
cal_dia2: 2.00
raw_dia1: 9500
raw_dia2: 10500
default_nominal_filament_diameter: 1.75
max_difference: 0.200
measurement_delay: 70
enable: False
measurement_interval: 10
logging: False
min_diameter: 1.0
use_current_dia_while_delay: False
sensor_type: hx711
gain: A-128
sample_rate: 80
sensor_type: hx717
gain: A-128
sample_rate: 320
sensor_type: ads1220
spi_speed: 512000
gain: 128
sample_rate: 660
smooth_time: 2.0
enable_pin: !gpio0_20
standstill_power_down: False
baud: 115200
feedrate_splice: 0.8
feedrate_normal: 1.0
auto_load_speed: 2
auto_cancel_variation: 0.1
sample_period: 0.000400

View File

@@ -0,0 +1,37 @@
[section]
[section with spaces]
[section with spaces and comments] ; comment 1
[section with spaces and comments] # comment 2
indented_option: value
option_with_no_value:
another_option_with_no_value:
indented_option_with_no_value:
# position_min: 0
# homing_speed: 5.0
### this is a comment
; this is also a comment
# [section]
# [section with spaces]
# [section with spaces and comments] ; comment 1
;[section]
;[section with spaces]
;[section with spaces and comments] ; comment 1
# commented_option: value
#commented_option: value
;commented_option: value
; commented_option: value
#
;
option_1 :: value
option_1:: value
option_1 ::value
option_2 == value
option_2== value
option_2 ==value
option_1 := value
option_1:= value
option_1 :=value
option_2 := value
option_2:= value
option_2 :=value

View File

@@ -0,0 +1,39 @@
# ======================================================================= #
# Copyright (C) 2024 Dominik Willner <th33xitus@gmail.com> #
# #
# https://github.com/dw-0/simple-config-parser #
# #
# This file may be distributed under the terms of the GNU GPLv3 license #
# ======================================================================= #
from pathlib import Path
import pytest
from src.simple_config_parser.simple_config_parser import SimpleConfigParser
from tests.utils import load_testdata_from_file
BASE_DIR = Path(__file__).parent.joinpath("test_data")
MATCHING_TEST_DATA_PATH = BASE_DIR.joinpath("matching_data.txt")
NON_MATCHING_TEST_DATA_PATH = BASE_DIR.joinpath("non_matching_data.txt")
@pytest.fixture
def parser():
return SimpleConfigParser()
@pytest.mark.parametrize("line", load_testdata_from_file(MATCHING_TEST_DATA_PATH))
def test_match_option(parser, line):
"""Test that a line matches the definition of an option"""
assert (
parser._match_option(line) is True
), f"Expected line '{line}' to match option definition!"
@pytest.mark.parametrize("line", load_testdata_from_file(NON_MATCHING_TEST_DATA_PATH))
def test_non_matching_option(parser, line):
"""Test that a line does not match the definition of an option"""
assert (
parser._match_option(line) is False
), f"Expected line '{line}' to not match option definition!"

View File

@@ -0,0 +1,15 @@
trusted_clients:
gcode:
cors_domains:
an_options_block_start_with_comment: ; this is a comment
an_options_block_start_with_comment: # this is a comment
options_block_start_with_comment:;this is a comment
options_block_start_with_comment :;this is a comment
options_block_start_with_comment:#this is a comment
options_block_start_with_comment :#this is a comment
parameter_temperature_(°C):
parameter_temperature_(°C)=
parameter_humidity_(%_RH):
parameter_humidity_(%_RH) :
parameter_spool_weight_(%):
parameter_spool_weight_(%) =

View File

@@ -0,0 +1,31 @@
type: jsonfile
path: /dev/shm/drying_box.json
baud: 250000
minimum_cruise_ratio: 0.5
square_corner_velocity: 5.0
full_steps_per_rotation: 200
position_min: 0
homing_speed: 5.0
# baud: 250000
# minimum_cruise_ratio: 0.5
# square_corner_velocity: 5.0
# full_steps_per_rotation: 200
# position_min: 0
# homing_speed: 5.0
### this is a comment
; this is also a comment
;
#
homing_speed::
homing_speed::
homing_speed ::
homing_speed ::
homing_speed==
homing_speed==
homing_speed ==
homing_speed ==
homing_speed :=
homing_speed :=
homing_speed =:
homing_speed =:

View File

@@ -0,0 +1,39 @@
# ======================================================================= #
# Copyright (C) 2024 Dominik Willner <th33xitus@gmail.com> #
# #
# https://github.com/dw-0/simple-config-parser #
# #
# This file may be distributed under the terms of the GNU GPLv3 license #
# ======================================================================= #
from pathlib import Path
import pytest
from src.simple_config_parser.simple_config_parser import SimpleConfigParser
from tests.utils import load_testdata_from_file
BASE_DIR = Path(__file__).parent.joinpath("test_data")
MATCHING_TEST_DATA_PATH = BASE_DIR.joinpath("matching_data.txt")
NON_MATCHING_TEST_DATA_PATH = BASE_DIR.joinpath("non_matching_data.txt")
@pytest.fixture
def parser():
return SimpleConfigParser()
@pytest.mark.parametrize("line", load_testdata_from_file(MATCHING_TEST_DATA_PATH))
def test_match_options_block_start(parser, line):
"""Test that a line matches the definition of an options block start"""
assert (
parser._match_options_block_start(line) is True
), f"Expected line '{line}' to match options block start definition!"
@pytest.mark.parametrize("line", load_testdata_from_file(NON_MATCHING_TEST_DATA_PATH))
def test_non_matching_options_block_start(parser, line):
"""Test that a line does not match the definition of an options block start"""
assert (
parser._match_options_block_start(line) is False
), f"Expected line '{line}' to not match options block start definition!"

View File

@@ -0,0 +1,127 @@
[example_section]
[gcode_macro CANCEL_PRINT]
[gcode_macro SET_PAUSE_NEXT_LAYER]
[gcode_macro _TOOLHEAD_PARK_PAUSE_CANCEL]
[update_manager moonraker-obico]
[include moonraker_obico_macros.cfg]
[include moonraker-obico-update.cfg]
[example_section two]
[valid_content]
[valid content]
[content123]
[a]
[valid_content] # comment
[something];comment
[mcu]
[printer]
[printer]
[stepper_x]
[stepper_y]
[stepper_z]
[printer]
[stepper_a]
[stepper_b]
[stepper_c]
[delta_calibrate]
[printer]
[stepper_left]
[stepper_right]
[stepper_bed]
[stepper_arm]
[delta_calibrate]
[extruder]
[heater_bed]
[bed_mesh]
[bed_tilt]
[bed_screws]
[screws_tilt_adjust]
[z_tilt]
[quad_gantry_level]
[skew_correction]
[z_thermal_adjust]
[safe_z_home]
[homing_override]
[endstop_phase stepper_z]
[gcode_macro my_cmd]
[delayed_gcode my_delayed_gcode]
[save_variables]
[idle_timeout]
[virtual_sdcard]
[sdcard_loop]
[force_move]
[pause_resume]
[firmware_retraction]
[gcode_arcs]
[respond]
[exclude_object]
[input_shaper]
[adxl345]
[lis2dw]
[mpu9250 my_accelerometer]
[resonance_tester]
[board_pins my_aliases]
[duplicate_pin_override]
[probe]
[bltouch]
[smart_effector]
[probe_eddy_current my_eddy_probe]
[axis_twist_compensation]
[stepper_z1]
[extruder1]
[dual_carriage]
[extruder_stepper my_extra_stepper]
[manual_stepper my_stepper]
[verify_heater heater_config_name]
[homing_heaters]
[thermistor my_thermistor]
[adc_temperature my_sensor]
[heater_generic my_generic_heater]
[temperature_sensor my_sensor]
[temperature_probe my_probe]
[fan]
[heater_fan heatbreak_cooling_fan]
[controller_fan my_controller_fan]
[temperature_fan my_temp_fan]
[fan_generic extruder_partfan]
[led my_led]
[neopixel my_neopixel]
[dotstar my_dotstar]
[pca9533 my_pca9533]
[pca9632 my_pca9632]
[servo my_servo]
[gcode_button my_gcode_button]
[output_pin my_pin]
[pwm_tool my_tool]
[pwm_cycle_time my_pin]
[static_digital_output my_output_pins]
[multi_pin my_multi_pin]
[tmc2130 stepper_x]
[tmc2208 stepper_x]
[tmc2209 stepper_x]
[tmc2660 stepper_x]
[tmc2240 stepper_x]
[tmc5160 stepper_x]
[ad5206 my_digipot]
[mcp4451 my_digipot]
[mcp4728 my_dac]
[mcp4018 my_digipot]
[display]
[display_data my_group_name my_data_name]
[display_template my_template_name]
[display_glyph my_display_glyph]
[menu __some_list __some_name]
[menu some_name]
[menu some_list]
[menu some_list some_command]
[menu some_list some_input]
[filament_switch_sensor my_sensor]
[filament_motion_sensor my_sensor]
[tsl1401cl_filament_width_sensor]
[hall_filament_width_sensor]
[load_cell]
[sx1509 my_sx1509]
[samd_sercom my_sercom]
[adc_scaled my_name]
[replicape]
[palette2]
[angle my_angle_sensor]

View File

@@ -0,0 +1,19 @@
section: invalid
not_a_valid_section
[missing_square_bracket
missing_square_bracket]
[]
[ ]
[indented_section]
[indented_section] # comment
[indented_section] ; comment
;[commented_section]
#[another_commented_section]
; [commented_section]
# [another_commented_section]
this_is_an_option: 123
this_is_an_indented_option: 123
this_is_an_option_block_start:
#
;

View File

@@ -0,0 +1,39 @@
# ======================================================================= #
# Copyright (C) 2024 Dominik Willner <th33xitus@gmail.com> #
# #
# https://github.com/dw-0/simple-config-parser #
# #
# This file may be distributed under the terms of the GNU GPLv3 license #
# ======================================================================= #
from pathlib import Path
import pytest
from src.simple_config_parser.simple_config_parser import SimpleConfigParser
from tests.utils import load_testdata_from_file
BASE_DIR = Path(__file__).parent.joinpath("test_data")
MATCHING_TEST_DATA_PATH = BASE_DIR.joinpath("matching_data.txt")
NON_MATCHING_TEST_DATA_PATH = BASE_DIR.joinpath("non_matching_data.txt")
@pytest.fixture
def parser():
return SimpleConfigParser()
@pytest.mark.parametrize("line", load_testdata_from_file(MATCHING_TEST_DATA_PATH))
def test_match_section(parser, line):
"""Test that a line matches the definition of a section"""
assert (
parser._match_section(line) is True
), f"Expected line '{line}' to match section definition!"
@pytest.mark.parametrize("line", load_testdata_from_file(NON_MATCHING_TEST_DATA_PATH))
def test_non_matching_section(parser, line):
"""Test that a line does not match the definition of a section"""
assert (
parser._match_section(line) is False
), f"Expected line '{line}' to not match section definition!"

View File

@@ -0,0 +1,62 @@
# ======================================================================= #
# Copyright (C) 2024 Dominik Willner <th33xitus@gmail.com> #
# #
# https://github.com/dw-0/simple-config-parser #
# #
# This file may be distributed under the terms of the GNU GPLv3 license #
# ======================================================================= #
from pathlib import Path
import pytest
from src.simple_config_parser.constants import HEADER_IDENT
from src.simple_config_parser.simple_config_parser import SimpleConfigParser
from tests.utils import load_testdata_from_file
BASE_DIR = Path(__file__).parent.parent.joinpath("assets")
TEST_DATA_PATH = BASE_DIR.joinpath("test_config_1.cfg")
@pytest.fixture
def parser():
parser = SimpleConfigParser()
for line in load_testdata_from_file(TEST_DATA_PATH):
parser._parse_line(line) # noqa
return parser
def test_section_parsing(parser):
expected_keys = {"section_1", "section_2", "section_3", "section_4"}
assert expected_keys.issubset(
parser.config.keys()
), f"Expected keys: {expected_keys}, got: {parser.config.keys()}"
assert parser.in_option_block is False
assert parser.current_section == "section number 5"
assert parser.config["section_2"]["_raw"] == "[section_2] ; comment"
def test_option_parsing(parser):
assert parser.config["section_1"]["option_1"]["value"] == "value_1"
assert parser.config["section_1"]["option_1"]["_raw"] == "option_1: value_1"
assert parser.config["section_3"]["option_3"]["value"] == "value_3"
assert (
parser.config["section_3"]["option_3"]["_raw"] == "option_3: value_3 # comment"
)
def test_header_parsing(parser):
header = parser.config[HEADER_IDENT]
assert isinstance(header, list)
assert len(header) > 0
def test_collector_parsing(parser):
section = "section_2"
section_content = list(parser.config[section].keys())
coll_name = [name for name in section_content if name.startswith("#_")][0]
collector = parser.config[section][coll_name]
assert collector is not None
assert isinstance(collector, list)
assert len(collector) > 0
assert "; comment" in collector

View File

@@ -0,0 +1,189 @@
# ======================================================================= #
# Copyright (C) 2024 Dominik Willner <th33xitus@gmail.com> #
# #
# https://github.com/dw-0/simple-config-parser #
# #
# This file may be distributed under the terms of the GNU GPLv3 license #
# ======================================================================= #
from pathlib import Path
import pytest
from src.simple_config_parser.simple_config_parser import (
NoOptionError,
NoSectionError,
SimpleConfigParser,
)
from tests.utils import load_testdata_from_file
BASE_DIR = Path(__file__).parent.parent.joinpath("assets")
TEST_DATA_PATH = BASE_DIR.joinpath("test_config_1.cfg")
@pytest.fixture
def parser():
parser = SimpleConfigParser()
for line in load_testdata_from_file(TEST_DATA_PATH):
parser._parse_line(line) # noqa
return parser
def test_get_options(parser):
expected_options = {
"section_1": {"option_1"},
"section_2": {"option_2"},
"section_3": {"option_3"},
"section_4": {"option_4"},
"section number 5": {"option_5", "multi_option", "option_5_1"},
}
for section, options in expected_options.items():
assert options.issubset(
parser.get_options(section)
), f"Expected options: {options} in section: {section}, got: {parser.get_options(section)}"
assert "_raw" not in parser.get_options(section)
assert all(
not option.startswith("#_") for option in parser.get_options(section)
)
def test_has_option(parser):
assert parser.has_option("section_1", "option_1") is True
assert parser.has_option("section_1", "option_128") is False
# section does not exist:
assert parser.has_option("section_128", "option_1") is False
def test_getval(parser):
# test regular option values
assert parser.getval("section_1", "option_1") == "value_1"
assert parser.getval("section_3", "option_3") == "value_3"
assert parser.getval("section_4", "option_4") == "value_4"
assert parser.getval("section number 5", "option_5") == "this.is.value-5"
assert parser.getval("section number 5", "option_5_1") == "value_5_1"
assert parser.getval("section_2", "option_2") == "value_2"
# test multiline option values
ml_val = parser.getval("section number 5", "multi_option")
assert isinstance(ml_val, list)
assert len(ml_val) > 0
def test_getval_fallback(parser):
assert parser.getval("section_1", "option_128", "fallback") == "fallback"
def test_getval_exceptions(parser):
with pytest.raises(NoSectionError):
parser.getval("section_128", "option_1")
with pytest.raises(NoOptionError):
parser.getval("section_1", "option_128")
def test_getint(parser):
value = parser.getint("section_1", "option_1_2")
assert isinstance(value, int)
def test_getint_from_val(parser):
with pytest.raises(ValueError):
parser.getint("section_1", "option_1")
def test_getint_from_float(parser):
with pytest.raises(ValueError):
parser.getint("section_1", "option_1_3")
def test_getint_from_boolean(parser):
with pytest.raises(ValueError):
parser.getint("section_1", "option_1_1")
def test_getint_fallback(parser):
assert parser.getint("section_1", "option_128", 128) == 128
def test_getboolean(parser):
value = parser.getboolean("section_1", "option_1_1")
assert isinstance(value, bool)
assert value is True or value is False
def test_getboolean_from_val(parser):
with pytest.raises(ValueError):
parser.getboolean("section_1", "option_1")
def test_getboolean_from_int(parser):
with pytest.raises(ValueError):
parser.getboolean("section_1", "option_1_2")
def test_getboolean_from_float(parser):
with pytest.raises(ValueError):
parser.getboolean("section_1", "option_1_3")
def test_getboolean_fallback(parser):
assert parser.getboolean("section_1", "option_128", True) is True
assert parser.getboolean("section_1", "option_128", False) is False
def test_getfloat(parser):
value = parser.getfloat("section_1", "option_1_3")
assert isinstance(value, float)
def test_getfloat_from_val(parser):
with pytest.raises(ValueError):
parser.getfloat("section_1", "option_1")
def test_getfloat_from_int(parser):
value = parser.getfloat("section_1", "option_1_2")
assert isinstance(value, float)
def test_getfloat_from_boolean(parser):
with pytest.raises(ValueError):
parser.getfloat("section_1", "option_1_1")
def test_getfloat_fallback(parser):
assert parser.getfloat("section_1", "option_128", 1.234) == 1.234
def test_set_existing_option(parser):
parser.set_option("section_1", "new_option", "new_value")
assert parser.getval("section_1", "new_option") == "new_value"
assert parser.config["section_1"]["new_option"]["_raw"] == "new_option: new_value\n"
parser.set_option("section_1", "new_option", "new_value_2")
assert parser.getval("section_1", "new_option") == "new_value_2"
assert (
parser.config["section_1"]["new_option"]["_raw"] == "new_option: new_value_2\n"
)
def test_set_new_option(parser):
parser.set_option("new_section", "very_new_option", "very_new_value")
assert (
parser.has_section("new_section") is True
), f"Expected 'new_section' in {parser.get_sections()}"
assert parser.getval("new_section", "very_new_option") == "very_new_value"
parser.set_option("section_2", "array_option", ["value_1", "value_2", "value_3"])
assert parser.getval("section_2", "array_option") == [
"value_1",
"value_2",
"value_3",
]
assert parser.config["section_2"]["array_option"]["_raw"] == "array_option:\n"
def test_remove_option(parser):
parser.remove_option("section_1", "option_1")
assert parser.has_option("section_1", "option_1") is False

View File

@@ -0,0 +1,28 @@
# ======================================================================= #
# Copyright (C) 2024 Dominik Willner <th33xitus@gmail.com> #
# #
# https://github.com/dw-0/simple-config-parser #
# #
# This file may be distributed under the terms of the GNU GPLv3 license #
# ======================================================================= #
from pathlib import Path
import pytest
from src.simple_config_parser.simple_config_parser import (
SimpleConfigParser,
)
BASE_DIR = Path(__file__).parent.parent.joinpath("assets")
TEST_DATA_PATH = BASE_DIR.joinpath("test_config_1.cfg")
@pytest.fixture
def parser():
return SimpleConfigParser()
def test_read_file(parser):
parser.read_file(TEST_DATA_PATH)
assert parser.config is not None
assert parser.config.keys() is not None

View File

@@ -0,0 +1,81 @@
# ======================================================================= #
# Copyright (C) 2024 Dominik Willner <th33xitus@gmail.com> #
# #
# https://github.com/dw-0/simple-config-parser #
# #
# This file may be distributed under the terms of the GNU GPLv3 license #
# ======================================================================= #
from pathlib import Path
import pytest
from src.simple_config_parser.simple_config_parser import (
DuplicateSectionError,
SimpleConfigParser,
)
from tests.utils import load_testdata_from_file
BASE_DIR = Path(__file__).parent.parent.joinpath("assets")
TEST_DATA_PATH = BASE_DIR.joinpath("test_config_1.cfg")
@pytest.fixture
def parser():
parser = SimpleConfigParser()
for line in load_testdata_from_file(TEST_DATA_PATH):
parser._parse_line(line) # noqa
return parser
def test_get_sections(parser):
expected_keys = {
"section_1",
"section_2",
"section_3",
"section_4",
"section number 5",
}
assert expected_keys.issubset(
parser.get_sections()
), f"Expected keys: {expected_keys}, got: {parser.get_sections()}"
def test_has_section(parser):
assert parser.has_section("section_1") is True
assert parser.has_section("not_available") is False
def test_add_section(parser):
pre_add_count = len(parser.get_sections())
parser.add_section("new_section")
parser.add_section("new_section2")
assert parser.has_section("new_section") is True
assert parser.has_section("new_section2") is True
assert len(parser.get_sections()) == pre_add_count + 2
new_section = parser.config["new_section"]
assert isinstance(new_section, dict)
assert new_section["_raw"] == "[new_section]\n"
# this should be the collector, added by the parser before
# then second section was added
assert list(new_section.keys())[-1].startswith("#_")
assert "\n" in new_section[list(new_section.keys())[-1]]
new_section2 = parser.config["new_section2"]
assert isinstance(new_section2, dict)
assert new_section2["_raw"] == "[new_section2]\n"
def test_add_section_duplicate(parser):
with pytest.raises(DuplicateSectionError):
parser.add_section("section_1")
def test_remove_section(parser):
pre_remove_count = len(parser.get_sections())
parser.remove_section("section_1")
assert parser.has_section("section_1") is False
assert len(parser.get_sections()) == pre_remove_count - 1
assert "section_1" not in parser.config

View File

@@ -0,0 +1,41 @@
# ======================================================================= #
# Copyright (C) 2024 Dominik Willner <th33xitus@gmail.com> #
# #
# https://github.com/dw-0/simple-config-parser #
# #
# This file may be distributed under the terms of the GNU GPLv3 license #
# ======================================================================= #
from pathlib import Path
import pytest
from src.simple_config_parser.simple_config_parser import (
SimpleConfigParser,
)
BASE_DIR = Path(__file__).parent.parent.joinpath("assets")
TEST_DATA_PATH = BASE_DIR.joinpath("test_config_1.cfg")
# TEST_DATA_PATH_2 = BASE_DIR.joinpath("test_config_1_write.cfg")
def test_write_file_exception():
parser = SimpleConfigParser()
with pytest.raises(ValueError):
parser.write_file(None) # noqa
def test_write_to_file(tmp_path):
tmp_file = Path(tmp_path).joinpath("tmp_config.cfg")
parser1 = SimpleConfigParser()
parser1.read_file(TEST_DATA_PATH)
# parser1.write_file(TEST_DATA_PATH_2)
parser1.write_file(tmp_file)
parser2 = SimpleConfigParser()
parser2.read_file(tmp_file)
assert tmp_file.exists()
assert parser2.config is not None
with open(TEST_DATA_PATH, "r") as original, open(tmp_file, "r") as written:
assert original.read() == written.read()

View File

@@ -0,0 +1,15 @@
# ======================================================================= #
# Copyright (C) 2024 Dominik Willner <th33xitus@gmail.com> #
# #
# https://github.com/dw-0/simple-config-parser #
# #
# This file may be distributed under the terms of the GNU GPLv3 license #
# ======================================================================= #
from pathlib import Path
def load_testdata_from_file(file_path: Path):
"""Helper function to load test data from a text file"""
with open(file_path, "r") as f:
return [line.replace("\n", "") for line in f]

View File

@@ -0,0 +1,74 @@
# ======================================================================= #
# Copyright (C) 2024 Dominik Willner <th33xitus@gmail.com> #
# #
# https://github.com/dw-0/simple-config-parser #
# #
# This file may be distributed under the terms of the GNU GPLv3 license #
# ======================================================================= #
from pathlib import Path
import pytest
from src.simple_config_parser.simple_config_parser import SimpleConfigParser
from tests.utils import load_testdata_from_file
BASE_DIR = Path(__file__).parent.parent.joinpath("assets")
TEST_DATA_PATH = BASE_DIR.joinpath("test_config_1.cfg")
@pytest.fixture
def parser():
parser = SimpleConfigParser()
for line in load_testdata_from_file(TEST_DATA_PATH):
parser._parse_line(line) # noqa
return parser
def test_get_conv(parser):
# Test conversion to int
should_be_int = parser._get_conv("section_1", "option_1_2", int)
assert isinstance(should_be_int, int)
# Test conversion to float
should_be_float = parser._get_conv("section_1", "option_1_3", float)
assert isinstance(should_be_float, float)
# Test conversion to boolean
should_be_bool = parser._get_conv(
"section_1", "option_1_1", parser._convert_to_boolean
)
assert isinstance(should_be_bool, bool)
# Test fallback for int
should_be_fallback_int = parser._get_conv(
"section_1", "option_128", int, fallback=128
)
assert isinstance(should_be_fallback_int, int)
assert should_be_fallback_int == 128
# Test fallback for float
should_be_fallback_float = parser._get_conv(
"section_1", "option_128", float, fallback=1.234
)
assert isinstance(should_be_fallback_float, float)
assert should_be_fallback_float == 1.234
# Test fallback for boolean
should_be_fallback_bool = parser._get_conv(
"section_1", "option_128", parser._convert_to_boolean, fallback=True
)
assert isinstance(should_be_fallback_bool, bool)
assert should_be_fallback_bool is True
# Test ValueError exception for invalid int conversion
with pytest.raises(ValueError):
parser._get_conv("section_1", "option_1", int)
# Test ValueError exception for invalid float conversion
with pytest.raises(ValueError):
parser._get_conv("section_1", "option_1", float)
# Test ValueError exception for invalid boolean conversion
with pytest.raises(ValueError):
parser._get_conv("section_1", "option_1", parser._convert_to_boolean)

View File

@@ -122,10 +122,10 @@ class GcodeShellCmdExtension(BaseExtension):
for cfg_file in cfg_files:
Logger.print_status(f"Include shell_command.cfg in '{cfg_file}' ...")
scp = SimpleConfigParser()
scp.read(cfg_file)
scp.read_file(cfg_file)
if scp.has_section(section):
Logger.print_info("Section already defined! Skipping ...")
continue
scp.add_section(section)
scp.write(cfg_file)
scp.write_file(cfg_file)
Logger.print_ok("Done!")

View File

@@ -141,5 +141,5 @@ class MoonrakerObico:
return False
scp = SimpleConfigParser()
scp.read(self.cfg_file)
return scp.get("server", "auth_token", None) is not None
scp.read_file(self.cfg_file)
return scp.getval("server", "auth_token", None) is not None

View File

@@ -281,15 +281,15 @@ class ObicoExtension(BaseExtension):
def _patch_obico_cfg(self, moonraker: Moonraker, obico: MoonrakerObico) -> None:
scp = SimpleConfigParser()
scp.read(obico.cfg_file)
scp.set("server", "url", self.server_url)
scp.set("moonraker", "port", str(moonraker.port))
scp.set(
scp.read_file(obico.cfg_file)
scp.set_option("server", "url", self.server_url)
scp.set_option("moonraker", "port", str(moonraker.port))
scp.set_option(
"logging",
"path",
obico.base.log_dir.joinpath(obico.log_file_name).as_posix(),
)
scp.write(obico.cfg_file)
scp.write_file(obico.cfg_file)
def _patch_printer_cfg(self, klipper: List[Klipper]) -> None:
add_config_section(

View File

@@ -35,7 +35,7 @@ def add_config_section(
continue
scp = SimpleConfigParser()
scp.read(cfg_file)
scp.read_file(cfg_file)
if scp.has_section(section):
Logger.print_info("Section already exist. Skipped ...")
continue
@@ -44,9 +44,9 @@ def add_config_section(
if options is not None:
for option in reversed(options):
scp.set(section, option[0], option[1])
scp.set_option(section, option[0], option[1])
scp.write(cfg_file)
scp.write_file(cfg_file)
def add_config_section_at_top(section: str, instances: List[InstanceType]) -> None:
@@ -55,9 +55,9 @@ def add_config_section_at_top(section: str, instances: List[InstanceType]) -> No
tmp_cfg = tempfile.NamedTemporaryFile(mode="w", delete=False)
tmp_cfg_path = Path(tmp_cfg.name)
scp = SimpleConfigParser()
scp.read(tmp_cfg_path)
scp.read_file(tmp_cfg_path)
scp.add_section(section)
scp.write(tmp_cfg_path)
scp.write_file(tmp_cfg_path)
tmp_cfg.close()
cfg_file = instance.cfg_file
@@ -80,10 +80,10 @@ def remove_config_section(section: str, instances: List[InstanceType]) -> None:
continue
scp = SimpleConfigParser()
scp.read(cfg_file)
scp.read_file(cfg_file)
if not scp.has_section(section):
Logger.print_info("Section does not exist. Skipped ...")
continue
scp.remove_section(section)
scp.write(cfg_file)
scp.write_file(cfg_file)