diff --git a/application_properties/application_properties.py b/application_properties/application_properties.py index 18cfb15..b9e6bc2 100644 --- a/application_properties/application_properties.py +++ b/application_properties/application_properties.py @@ -94,7 +94,10 @@ def clear(self) -> None: self.__flat_property_map.clear() def load_from_dict( - self, config_map: Dict[Any, Any], clear_map: bool = True + self, + config_map: Dict[Any, Any], + clear_map: bool = True, + allow_periods_in_keys: bool = False, ) -> None: """ Load the properties from a provided dictionary. @@ -106,7 +109,7 @@ def load_from_dict( LOGGER.debug("Loading from dictionary: {%s}", str(config_map)) if clear_map: self.clear() - self.__scan_map(config_map, "") + self.__scan_map(config_map, "", allow_periods_in_keys) @staticmethod def verify_full_part_form(property_key: str) -> str: @@ -444,7 +447,13 @@ def property_names_under(self, key_name: str) -> List[str]: if next_key_name.startswith(key_name) ] - def __scan_map(self, config_map: Dict[Any, Any], current_prefix: str) -> None: + # pylint: disable=too-many-boolean-expressions + def __scan_map( + self, + config_map: Dict[Any, Any], + current_prefix: str, + allow_periods_in_keys: bool, + ) -> None: for next_key, next_value in config_map.items(): if not isinstance(next_key, str): raise ValueError( @@ -455,17 +464,24 @@ def __scan_map(self, config_map: Dict[Any, Any], current_prefix: str) -> None: or "\t" in next_key or "\n" in next_key or ApplicationProperties.__assignment_operator in next_key - or ApplicationProperties.__separator in next_key + or ( + ApplicationProperties.__separator in next_key + and not allow_periods_in_keys + ) ): raise ValueError( "Key strings cannot contain a whitespace character, " + f"a '{ApplicationProperties.__assignment_operator}' character, or " + f"a '{ApplicationProperties.__separator}' character." ) + if ApplicationProperties.__separator in next_key: + next_key = f"'{next_key}'" if isinstance(next_value, dict): self.__scan_map( - next_value, f"{current_prefix}{next_key}{self.__separator}" + next_value, + f"{current_prefix}{next_key}{self.__separator}", + allow_periods_in_keys, ) else: new_key = f"{current_prefix}{next_key}".lower() @@ -473,3 +489,5 @@ def __scan_map(self, config_map: Dict[Any, Any], current_prefix: str) -> None: LOGGER.debug( "Adding configuration '%s' : {%s}", new_key, str(next_value) ) + + # pylint: enable=too-many-boolean-expressions diff --git a/application_properties/application_properties_toml_loader.py b/application_properties/application_properties_toml_loader.py index e1cd0fd..b8981ac 100644 --- a/application_properties/application_properties_toml_loader.py +++ b/application_properties/application_properties_toml_loader.py @@ -73,7 +73,9 @@ def load_and_set( ) if configuration_map: properties_object.load_from_dict( - configuration_map, clear_map=clear_property_map + configuration_map, + clear_map=clear_property_map, + allow_periods_in_keys=True, ) did_apply_map = True return did_apply_map and not did_have_one_error, did_have_one_error diff --git a/application_properties/multisource_configuration_loader.py b/application_properties/multisource_configuration_loader.py index 4223981..b7c3504 100644 --- a/application_properties/multisource_configuration_loader.py +++ b/application_properties/multisource_configuration_loader.py @@ -56,6 +56,9 @@ class MultisourceConfigurationLoaderOptions: """ Allow parsing of JSON files using the JSON5 parser. (Default = False) """ + section_header_if_toml: Optional[str] = None + """Optional section header to use when loading TOML configuration files. + """ # pylint: disable=too-few-public-methods @@ -137,6 +140,7 @@ def __load_as_yaml( def __load_as_toml( self, file_name: str, + options: MultisourceConfigurationLoaderOptions, application_properties: ApplicationProperties, handle_error_fn: Callable[[str, Optional[Exception]], None], ) -> Tuple[bool, bool]: @@ -151,6 +155,7 @@ def __load_as_toml( ) = ApplicationPropertiesTomlLoader.load_and_set( application_properties, file_name, + section_header=options.section_header_if_toml, handle_error_fn=handle_error_fn, clear_property_map=False, check_for_file_presence=True, @@ -179,7 +184,9 @@ def _load_config( file_name, application_properties, handle_error_fn ) assert config_file_type == ConfigurationFileType.TOML - return self.__load_as_toml(file_name, application_properties, handle_error_fn) + return self.__load_as_toml( + file_name, options, application_properties, handle_error_fn + ) # return False, False # pylint: enable=too-many-arguments @@ -604,6 +611,8 @@ def add_specified_configuration_file( is made to load the file. config_file_type: Type of configuration file to load. For auto-detecting the file type, see the notes above. + section_header_if_toml: Optional section header to use when loading + TOML configuration files. Returns: Instance of `self` for chaining `add_*` functions and the `process` function diff --git a/application_properties/version.py b/application_properties/version.py index 8a481da..ff43cdf 100644 --- a/application_properties/version.py +++ b/application_properties/version.py @@ -2,7 +2,7 @@ Library version information. """ -__version__ = "0.9.0" +__version__ = "0.9.1" __project_name__ = "application_properties" __description__ = ( "A simple, easy to use, unified manner of accessing program properties." diff --git a/clean.sh b/clean.sh index c17abdb..59b4c74 100644 --- a/clean.sh +++ b/clean.sh @@ -84,6 +84,7 @@ show_usage() { echo " -m,--mypy-only Only run mypy checks and exit." echo " -np,--no-publish Do not publish project summaries if successful." echo " -ns,--no-sourcery Do not run any sourcery checks." + echo " -nu,--no-upgrades Do not run checks for upgrades." echo " -s,--sourcery-only Only run sourcery checks and exit." echo " --perf Collect standard performance metrics." echo " --perf-only Only collect standard performance metrics." @@ -105,6 +106,7 @@ parse_command_line() { MYPY_ONLY_MODE=0 SOURCERY_ONLY_MODE=0 NO_SOURCERY_MODE=0 + NO_UPGRADE_MODE=0 FORCE_RESET_MODE=0 RESET_PYTHON_VERSION= PARAMS=() @@ -131,6 +133,10 @@ parse_command_line() { NO_SOURCERY_MODE=1 shift ;; + -nu | --no-upgrades) + NO_UPGRADE_MODE=1 + shift + ;; -s | --sourcery-only) SOURCERY_ONLY_MODE=1 shift @@ -404,6 +410,11 @@ analyze_pylint_suppressions() { look_for_upgrades() { + if [[ ${NO_UPGRADE_MODE} -ne 0 ]]; then + verbose_echo "{Skipping check for Python package upgrades by request.}" + return + fi + verbose_echo "{Looking for Python package upgrades in Pre-Commit and Pipenv.}" if ! ./check_project_dependencies.sh; then complete_process 1 "{One or more project dependencies can be updated. Please run './check_project_dependencies.sh --upgrade' to update them.}" diff --git a/newdocs/src/changelog.md b/newdocs/src/changelog.md index 8d01254..c6d8973 100644 --- a/newdocs/src/changelog.md +++ b/newdocs/src/changelog.md @@ -16,3 +16,20 @@ ### Fixed - None + +## Version 0.9.1 - Date: 2026-01-24 + + +### Fixed + +- [Improper parsing of TOML](https://github.com/jackdewinter/application_properties/issues/269) + - The `.` character is considered a separator character for keys, with the code + checking to prevent that character from being used within a key. As TOML + will already break the key on that character, a bypass was added to allow the + TOML keys to include a `.` character IF it is in quotes. +- [Config flag fails to apply pyproject TOML](https://github.com/jackdewinter/application_properties/issues/318) + - If the `pyproject.toml` file was loaded implicitly, the file was loading + and processing the TOML file to only look at the "section header" that was + specified. If passed using `--config`, it was not. Added the `section_header_if_toml` + field to the `MultisourceConfigurationLoaderOptions` class to allow an optional + section header to be passed in when loading an "untyped" configuration file. diff --git a/publish/coverage.json b/publish/coverage.json index 88072a2..dac98a3 100644 --- a/publish/coverage.json +++ b/publish/coverage.json @@ -2,12 +2,12 @@ "projectName": "application_properties", "reportSource": "Unknown", "branchLevel": { - "totalMeasured": 178, - "totalCovered": 178 + "totalMeasured": 180, + "totalCovered": 180 }, "lineLevel": { - "totalMeasured": 665, - "totalCovered": 665 + "totalMeasured": 669, + "totalCovered": 669 } } diff --git a/publish/pylint_suppression.json b/publish/pylint_suppression.json index e4735a2..fbf31f0 100644 --- a/publish/pylint_suppression.json +++ b/publish/pylint_suppression.json @@ -3,7 +3,8 @@ "application_properties/__init__.py": {}, "application_properties/application_properties.py": { "too-many-arguments": 4, - "broad-exception-caught": 1 + "broad-exception-caught": 1, + "too-many-boolean-expressions": 1 }, "application_properties/application_properties_config_loader.py": { "too-few-public-methods": 1, @@ -39,6 +40,7 @@ "disables-by-name": { "too-many-arguments": 14, "broad-exception-caught": 1, + "too-many-boolean-expressions": 1, "too-few-public-methods": 10, "no-member": 2 } diff --git a/publish/test-results.json b/publish/test-results.json index fbbc996..8e378ee 100644 --- a/publish/test-results.json +++ b/publish/test-results.json @@ -36,7 +36,7 @@ }, { "name": "test.test_application_properties_toml_loader", - "totalTests": 17, + "totalTests": 22, "failedTests": 0, "errorTests": 0, "skippedTests": 0, diff --git a/test/patches/patch_base.py b/test/patches/patch_base.py new file mode 100644 index 0000000..ab43ed4 --- /dev/null +++ b/test/patches/patch_base.py @@ -0,0 +1,55 @@ +""" +Module to provide a base class for any patch classes. +""" + +import unittest +from typing import Any, List, Optional, Union +from unittest.mock import AsyncMock, MagicMock, _patch + + +class PatchBase: + """ + Class to provide a base class for any patch classes. + """ + + def __init__(self, function_name_to_patch: str) -> None: + self.__mock_patcher: Optional[_patch[Union[MagicMock, AsyncMock]]] = None + self.__patched_function: Optional[MagicMock] = None + self.__function_name_to_patch = function_name_to_patch + self.__action_comments: List[str] = [] + + def _add_action_comment(self, comment_to_add: str) -> None: + # sourcery skip: remove-unnecessary-cast + self.__action_comments.append(str(comment_to_add)) + + def _add_side_effect(self, callable_function: Any) -> None: + assert self.__patched_function is not None + self.__patched_function.side_effect = callable_function + + def start(self, log_action: bool = True) -> None: + """ + Start the capturing of calls and apply any needed mocking. + """ + if log_action: + self._add_action_comment("starting") + self.__mock_patcher = unittest.mock.patch(self.__function_name_to_patch) + assert self.__mock_patcher is not None + self.__patched_function = self.__mock_patcher.start() + + def stop( + self, log_action: bool = True, print_action_comments: bool = False + ) -> None: + """ + Stop the capturing of calls and any needed mocking. + """ + assert self.__mock_patcher is not None + if log_action: + self._add_action_comment("stopping") + + self.__mock_patcher.stop() + self.__mock_patcher = None + self.__patched_function = None + if log_action: + self._add_action_comment("stopped") + if print_action_comments: + print("\n".join(self.__action_comments)) diff --git a/test/patches/patch_builtin_open.py b/test/patches/patch_builtin_open.py new file mode 100644 index 0000000..e4e3036 --- /dev/null +++ b/test/patches/patch_builtin_open.py @@ -0,0 +1,157 @@ +""" +Module to patch the "builtin.open" function. +""" + +import unittest.mock +from contextlib import contextmanager +from test.patches.patch_base import PatchBase +from typing import Any, Dict, Generator, Tuple + + +class PatchBuiltinOpen(PatchBase): + """ + Class to patch the "builtin.open" function. + """ + + def __init__(self) -> None: + super().__init__("builtins.open") + self.mock_patcher = None + self.patched_open = None + self.open_file_args = None + + self.content_map: Dict[str, str] = {} + self.binary_content_map: Dict[str, bytes] = {} + self.exception_map: Dict[str, Tuple[str, Exception]] = {} + + def start(self, log_action: bool = True) -> None: + """ + Start the patching of the "open" function. + """ + super().start(log_action=log_action) + + self._add_side_effect(self.__my_open) + if log_action: + self._add_action_comment(f"started: map={self.exception_map}") + + def stop( + self, log_action: bool = True, print_action_comments: bool = False + ) -> None: + """ + Stop the patching of the "open" function. + """ + super().stop(log_action=log_action, print_action_comments=print_action_comments) + + def register_text_content_for_file( + self, exact_file_name: str, file_contents: str + ) -> None: + """ + Register text content to return when the specified file is opened for reading as + a text file. + """ + self.content_map[exact_file_name] = file_contents + if len(file_contents) > 20: + file_contents = f"{file_contents[:19]}..." + self._add_action_comment( + f"register_text_content[{exact_file_name}]=[{file_contents}]]" + ) + + def register_binary_content_for_file( + self, exact_file_name: str, file_contents: bytes + ) -> None: + """ + Register text content to return when the specified file is opened for reading as + a text file. + """ + self.binary_content_map[exact_file_name] = file_contents + self._add_action_comment(f"register_binary_content[{exact_file_name}]=[]]") + + def register_exception_for_file( + self, exact_file_name: str, file_mode: str, exception_to_throw: Exception + ) -> None: + """ + Register an exception to raise when the specified file is opened with the given mode. + """ + self.exception_map[exact_file_name] = (file_mode, exception_to_throw) + self._add_action_comment( + f"register_exception[{exact_file_name}]=[{file_mode}],[{type(exception_to_throw)}]" + ) + + def __my_open(self, *args: Any, **kwargs: Any) -> Any: + """ + Provide alternate handling of the "builtins.open" function. + """ + filename = args[0] + filemode = args[1] if len(args) > 1 else "r" + if filename in self.content_map and filemode == "r": + self._add_action_comment("text-content-match") + + content = self.content_map[filename] + file_object = unittest.mock.mock_open(read_data=content).return_value + file_object.__iter__.return_value = content.splitlines(True) + return file_object + + if filename in self.binary_content_map and filemode == "rb": + self._add_action_comment("binary-content-match") + + binary_content = self.binary_content_map[filename] + file_object = unittest.mock.mock_open(read_data=binary_content).return_value + # file_object.__iter__.return_value = content.splitlines(True) + return file_object + + if filename in self.exception_map: + match_filemode, exception_to_throw = self.exception_map[filename] + if filemode == match_filemode: + self._add_action_comment(str((args, "exception-match"))) + raise exception_to_throw + self._add_action_comment(str((args, "exception-mode-mismatch"))) + + # pylint: disable=unspecified-encoding + self.stop(log_action=False) + try: + self._add_action_comment(f"passthrough = [{args}]") + + return open( + filename, + filemode, + **kwargs, + ) + finally: + self.start(log_action=False) + # pylint: enable=unspecified-encoding + + +@contextmanager +def path_builtin_open_with_binary_content( + exception_path: str, + content_to_return: bytes, + print_action_comments: bool = False, +) -> Generator[None, None, None]: + """ + Patch the builtin.open function, registering an exception to be thrown. + """ + patch = PatchBuiltinOpen() + patch.register_binary_content_for_file(exception_path, content_to_return) + patch.start() + try: + yield + finally: + patch.stop(print_action_comments=print_action_comments) + + +@contextmanager +def path_builtin_open_with_exception( + exception_path: str, + file_mode: str, + exception_to_throw: Exception, + print_action_comments: bool = False, +) -> Generator[None, None, None]: + """ + Patch the builtin.open function, registering an exception to be thrown. + """ + patch = PatchBuiltinOpen() + patch.register_exception_for_file(exception_path, file_mode, exception_to_throw) + patch.start() + try: + yield + finally: + patch.stop(print_action_comments=print_action_comments) diff --git a/test/test_application_properties_toml_loader.py b/test/test_application_properties_toml_loader.py index bca2ede..5290926 100644 --- a/test/test_application_properties_toml_loader.py +++ b/test/test_application_properties_toml_loader.py @@ -5,12 +5,17 @@ import io import os import sys +from test.patches.patch_builtin_open import path_builtin_open_with_binary_content from test.pytest_helpers import ErrorResults, TestHelpers from application_properties import ApplicationProperties from application_properties.application_properties_toml_loader import ( ApplicationPropertiesTomlLoader, ) +from application_properties.multisource_configuration_loader import ( + MultisourceConfigurationLoader, + MultisourceConfigurationLoaderOptions, +) def test_toml_loader_config_not_present() -> None: @@ -315,6 +320,7 @@ def test_toml_loader_toml_file_not_valid() -> None: finally: sys.stdout = old_stdout + # Assert assert expected_did_apply == actual_did_apply assert expected_did_error == actual_did_error assert std_output.getvalue() is not None @@ -358,6 +364,7 @@ def test_toml_loader_toml_file_valid_with_no_section_header() -> None: "plugins.tools.bar", None, None ) + # Assert assert expected_did_apply == actual_did_apply assert expected_did_error == actual_did_error assert expected_value == actual_value @@ -405,6 +412,7 @@ def test_toml_loader_toml_file_valid_with_one_word_section_header() -> None: "tools.bar", None, None ) + # Assert assert expected_did_apply == actual_did_apply assert expected_did_error == actual_did_error assert expected_value == actual_value @@ -452,6 +460,7 @@ def test_toml_loader_toml_file_valid_with_one_word_bad_section_header() -> None: "tools.bar", None, None ) + # Assert assert expected_did_apply == actual_did_apply assert expected_did_error == actual_did_error assert expected_value == actual_value @@ -498,6 +507,7 @@ def test_toml_loader_toml_file_valid_with_multi_word_valid_section_header() -> N "tools.bar", None, None ) + # Assert assert expected_did_apply == actual_did_apply assert expected_did_error == actual_did_error assert expected_value == actual_value @@ -546,6 +556,7 @@ def test_toml_loader_toml_file_valid_with_multi_word_section_header_that_points_ "tools.bar", None, None ) + # Assert assert expected_did_apply == actual_did_apply assert expected_did_error == actual_did_error assert expected_value == actual_value @@ -591,6 +602,7 @@ def test_toml_loader_toml_file_bad_toml_format_with_repeated_section() -> None: False, ) + # Assert assert expected_did_apply == actual_did_apply assert expected_did_error == actual_did_error assert results.reported_error is not None @@ -639,6 +651,7 @@ def test_toml_loader_toml_file_bad_toml_format_with_header_with_leading_period() False, ) + # Assert assert expected_did_apply == actual_did_apply assert expected_did_error == actual_did_error assert results.reported_error is not None @@ -685,6 +698,7 @@ def test_toml_loader_toml_file_bad_toml_format_with_item_with_leading_period() - False, ) + # Assert assert expected_did_apply == actual_did_apply assert expected_did_error == actual_did_error assert results.reported_error is not None @@ -736,6 +750,7 @@ def test_toml_loader_toml_file_bad_toml_format_with_double_items_through_differe False, ) + # Assert assert expected_did_apply == actual_did_apply assert expected_did_error == actual_did_error assert results.reported_error is not None @@ -746,3 +761,232 @@ def test_toml_loader_toml_file_bad_toml_format_with_double_items_through_differe finally: if configuration_file and os.path.exists(configuration_file): os.remove(configuration_file) + + +def test_toml_loader_key_with_quoted_separator() -> None: + """ + Test to make sure that we can load a toml file that contains a key with a quoted separator. + This is because the TOML format will not allow unquoted periods in keys, thus handling the + separator issue for us. + """ + + # Arrange + section_header = None + supplied_configuration = """[tool.ruff] + +lint.per-file-ignores."my_proj/logger.py" = "1" +""" + results = ErrorResults() + expected_did_apply = True + expected_did_error = False + expected_value = "1" + + configuration_file = None + try: + configuration_file = TestHelpers.write_temporary_configuration( + supplied_configuration + ) + application_properties = ApplicationProperties() + + # Act + ( + actual_did_apply, + actual_did_error, + ) = ApplicationPropertiesTomlLoader.load_and_set( + application_properties, + configuration_file, + section_header, + results.keep_error, + True, + False, + ) + actual_value = application_properties.get_string_property( + "tool.ruff.lint.per-file-ignores.'my_proj/logger.py'" + ) + + # Assert + assert expected_did_apply == actual_did_apply + assert expected_did_error == actual_did_error + assert expected_value == actual_value + + finally: + if configuration_file and os.path.exists(configuration_file): + os.remove(configuration_file) + + +def test_toml_loader_key_with_unquoted_separator_and_slash() -> None: + """ + Test to make sure that the same data from `test_toml_loader_key_with_quoted_separator` + without the quotes causes an error, as TOML does not allow unquoted periods and slashes in keys. + """ + + # Arrange + section_header = None + supplied_configuration = """[tool.ruff] + +lint.per-file-ignores.my_proj/logger.py = "1" +""" + results = ErrorResults() + expected_did_apply = False + expected_did_error = True + + configuration_file = None + try: + configuration_file = TestHelpers.write_temporary_configuration( + supplied_configuration + ) + application_properties = ApplicationProperties() + + # Act + ( + actual_did_apply, + actual_did_error, + ) = ApplicationPropertiesTomlLoader.load_and_set( + application_properties, + configuration_file, + section_header, + results.keep_error, + True, + False, + ) + + # Assert + assert expected_did_apply == actual_did_apply + assert expected_did_error == actual_did_error + assert results.reported_error is not None + assert ( + results.reported_error + == f"Specified configuration file '{configuration_file}' is not a valid TOML file: Expected '=' after a key in a key/value pair (at line 3, column 30)." + ) + finally: + if configuration_file and os.path.exists(configuration_file): + os.remove(configuration_file) + + +def test_toml_loader_key_with_quoted_separator_and_slash_but_not_supported_type() -> ( + None +): + """ + Test to make sure that we can still load a key with a quoted separator and slash, + but that the type is not supported for conversion, thus returning the default value. + """ + + # Arrange + section_header = None + supplied_configuration = """[tool.ruff] + +lint.per-file-ignores."my_proj/logger.py" = [ + "ANN001", # No type check of logging functions needed +] +""" + results = ErrorResults() + expected_value = -1 + expected_did_apply = True + expected_did_error = False + + configuration_file = None + try: + configuration_file = TestHelpers.write_temporary_configuration( + supplied_configuration + ) + application_properties = ApplicationProperties() + + # Act + ( + actual_did_apply, + actual_did_error, + ) = ApplicationPropertiesTomlLoader.load_and_set( + application_properties, + configuration_file, + section_header, + results.keep_error, + True, + False, + ) + actual_value = application_properties.get_integer_property( + "tool.ruff.lint.per-file-ignores.'my_proj/logger.py'", -1 + ) + + # Assert + assert expected_did_apply == actual_did_apply + assert expected_did_error == actual_did_error + assert expected_value == actual_value + + finally: + if configuration_file and os.path.exists(configuration_file): + os.remove(configuration_file) + + +def test_toml_loader_load_toml_implicitly_from_pyproject() -> None: + """ + Test to make sure that we can load a toml file from an implied pyproject.toml file. + """ + + # Arrange + results = ErrorResults() + __pyproject_section_header = "tool.pymarkdown" + supplied_configuration = """[tool.pymarkdown] +plugins.md013.enabled = true +plugins.md013.line_length = 50 +""" + expected_value = 50 + + pyproject_toml_path = os.path.abspath("pyproject.toml") + + application_properties = ApplicationProperties() + + # Act + with path_builtin_open_with_binary_content( + pyproject_toml_path, supplied_configuration.encode("utf-8") + ): + loader = MultisourceConfigurationLoader() + loader.add_local_pyproject_toml_file(__pyproject_section_header) + loader.process(application_properties, results.keep_error) + + actual_value = application_properties.get_integer_property( + "plugins.md013.line_length", -1 + ) + + # Assert + assert expected_value == actual_value + + +def test_toml_loader_load_toml_explicitly_from_configuration_file() -> None: + """ + Test to make sure that we can load a toml file from an explicitly named configuration file. + """ + + # Arrange + results = ErrorResults() + __pyproject_section_header = "tool.pymarkdown" + supplied_configuration = """[tool.pymarkdown] +plugins.md013.enabled = true +plugins.md013.line_length = 50 +""" + expected_value = 50 + + configuration_file = None + try: + configuration_file = TestHelpers.write_temporary_configuration( + supplied_configuration + ) + application_properties = ApplicationProperties() + + # Act + options = MultisourceConfigurationLoaderOptions( + section_header_if_toml=__pyproject_section_header + ) + loader = MultisourceConfigurationLoader(options) + loader.add_specified_configuration_file(configuration_file) + loader.process(application_properties, results.keep_error) + + actual_value = application_properties.get_integer_property( + "plugins.md013.line_length", -1 + ) + + # Assert + assert expected_value == actual_value + + finally: + if configuration_file and os.path.exists(configuration_file): + os.remove(configuration_file)