From c6d5f6fd7e8df8f331463a96fe2e096128a79f3a Mon Sep 17 00:00:00 2001 From: Aseem Saxena Date: Fri, 10 Oct 2025 13:13:08 -0700 Subject: [PATCH 01/10] wip --- codeflash/code_utils/code_utils.py | 16 +++++++++++--- codeflash/code_utils/config_parser.py | 31 +++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/codeflash/code_utils/code_utils.py b/codeflash/code_utils/code_utils.py index c71b9dc29..89279b2b9 100644 --- a/codeflash/code_utils/code_utils.py +++ b/codeflash/code_utils/code_utils.py @@ -19,6 +19,8 @@ ImportErrorPattern = re.compile(r"ModuleNotFoundError.*$", re.MULTILINE) +BLACKLIST_ADDOPTS = ("benchmark", "sugar", "codespeed", "cov", "profile", "junitxml") + def unified_diff_strings(code1: str, code2: str, fromfile: str = "original", tofile: str = "modified") -> str: """Return the unified diff between two code strings as a single string. @@ -84,6 +86,7 @@ def create_rank_dictionary_compact(int_array: list[int]) -> dict[int, int]: @contextmanager def custom_addopts() -> None: pyproject_file = find_pyproject_toml() + # closest_config_files = get_all_closest_config_files() original_content = None non_blacklist_plugin_args = "" @@ -97,9 +100,16 @@ def custom_addopts() -> None: original_addopts = data.get("tool", {}).get("pytest", {}).get("ini_options", {}).get("addopts", "") # nothing to do if no addopts present if original_addopts != "" and isinstance(original_addopts, list): - original_addopts = [x.strip() for x in original_addopts] - non_blacklist_plugin_args = re.sub(r"-n(?: +|=)\S+", "", " ".join(original_addopts)).split(" ") - non_blacklist_plugin_args = [x for x in non_blacklist_plugin_args if x != ""] + non_blacklist_plugin_args = [] + for opt in original_addopts: + opt_stripped = opt.strip().lstrip("-") + # Filter out -n/--numprocesses and blacklisted options + if opt_stripped.startswith(("n=", "numprocesses=")) or any( + opt_stripped.startswith(b) for b in BLACKLIST_ADDOPTS + ): + continue + non_blacklist_plugin_args.append(opt) + if non_blacklist_plugin_args != original_addopts: data["tool"]["pytest"]["ini_options"]["addopts"] = non_blacklist_plugin_args # Write modified file diff --git a/codeflash/code_utils/config_parser.py b/codeflash/code_utils/config_parser.py index b3f3495d6..7e877d03d 100644 --- a/codeflash/code_utils/config_parser.py +++ b/codeflash/code_utils/config_parser.py @@ -5,6 +5,8 @@ import tomlkit +ALL_CONFIG_FILES = {} # map path to closest config file + def find_pyproject_toml(config_file: Path | None = None) -> Path: # Find the pyproject.toml file on the root of the project @@ -31,6 +33,35 @@ def find_pyproject_toml(config_file: Path | None = None) -> Path: raise ValueError(msg) +def get_all_closest_config_files() -> list[Path]: + all_closest_config_files = [] + for file_type in ["pyproject.toml", "pytest.ini", ".pytest.ini", "tox.ini", "setup.cfg"]: + closest_config_file = find_closest_config_file(file_type) + if closest_config_file: + all_closest_config_files.append(closest_config_file) + return all_closest_config_files + + +def find_closest_config_file(file_type: str) -> Path: + # Find the closest pyproject.toml, pytest.ini, tox.ini, or setup.cfg file on the root of the project + dir_path = Path.cwd() + cur_path = dir_path + if cur_path in ALL_CONFIG_FILES and file_type in ALL_CONFIG_FILES[cur_path]: + return ALL_CONFIG_FILES[cur_path][file_type] + while dir_path != dir_path.parent: + config_file = dir_path / file_type + if config_file.exists(): + if cur_path not in ALL_CONFIG_FILES: + ALL_CONFIG_FILES[cur_path] = {} + ALL_CONFIG_FILES[cur_path][file_type] = config_file + return config_file + # Search for pyproject.toml in the parent directories + dir_path = dir_path.parent + msg = f"Could not find pyproject.toml in the current directory {Path.cwd()} or any of the parent directories. Please create it by running `codeflash init`, or pass the path to pyproject.toml with the --config-file argument." + + raise ValueError(msg) + + def find_conftest_files(test_paths: list[Path]) -> list[Path]: list_of_conftest_files = set() for test_path in test_paths: From c6da4fd53cee7ef1f913e736012df183ac186b37 Mon Sep 17 00:00:00 2001 From: Aseem Saxena Date: Fri, 10 Oct 2025 14:45:24 -0700 Subject: [PATCH 02/10] wip --- codeflash/code_utils/code_utils.py | 79 ++++++++++++++++-------------- 1 file changed, 42 insertions(+), 37 deletions(-) diff --git a/codeflash/code_utils/code_utils.py b/codeflash/code_utils/code_utils.py index 89279b2b9..e264cf801 100644 --- a/codeflash/code_utils/code_utils.py +++ b/codeflash/code_utils/code_utils.py @@ -1,6 +1,7 @@ from __future__ import annotations import ast +import configparser import difflib import os import re @@ -15,7 +16,7 @@ import tomlkit from codeflash.cli_cmds.console import logger, paneled_text -from codeflash.code_utils.config_parser import find_pyproject_toml +from codeflash.code_utils.config_parser import find_pyproject_toml, get_all_closest_config_files ImportErrorPattern = re.compile(r"ModuleNotFoundError.*$", re.MULTILINE) @@ -83,50 +84,54 @@ def create_rank_dictionary_compact(int_array: list[int]) -> dict[int, int]: return {original_index: rank for rank, original_index in enumerate(sorted_indices)} +def modify_addopts(config_file: Path) -> tuple[str, bool]: + content = "" + try: + if config_file.suffix.lower() == "toml": + # use tomlkit + pass + else: + # use configparser + pass + except Exception: + logger.debug("Trouble parsing") + return content, False # not modified + config = configparser.ConfigParser() + config.read(config_file) + # read file + # parse + # modify + # save + # return original content + print(config_file) + return "", True + + @contextmanager def custom_addopts() -> None: - pyproject_file = find_pyproject_toml() - # closest_config_files = get_all_closest_config_files() - original_content = None - non_blacklist_plugin_args = "" + closest_config_files = get_all_closest_config_files() - try: - # Read original file - if pyproject_file.exists(): - with Path.open(pyproject_file, encoding="utf-8") as f: - original_content = f.read() - data = tomlkit.parse(original_content) - # Backup original addopts - original_addopts = data.get("tool", {}).get("pytest", {}).get("ini_options", {}).get("addopts", "") - # nothing to do if no addopts present - if original_addopts != "" and isinstance(original_addopts, list): - non_blacklist_plugin_args = [] - for opt in original_addopts: - opt_stripped = opt.strip().lstrip("-") - # Filter out -n/--numprocesses and blacklisted options - if opt_stripped.startswith(("n=", "numprocesses=")) or any( - opt_stripped.startswith(b) for b in BLACKLIST_ADDOPTS - ): - continue - non_blacklist_plugin_args.append(opt) - - if non_blacklist_plugin_args != original_addopts: - data["tool"]["pytest"]["ini_options"]["addopts"] = non_blacklist_plugin_args - # Write modified file - with Path.open(pyproject_file, "w", encoding="utf-8") as f: - f.write(tomlkit.dumps(data)) + # 1. find closest config files + # 2. iterate through each of them and mask the addopts + # 3. yield + # 4. restore the original addopts when the context manager exits + original_content = {} + + try: + for config_file in closest_config_files: + # Read original file + print(config_file) + # if pyproject_file.exists(): + original_content[config_file] = modify_addopts(config_file) yield finally: # Restore original file - if ( - original_content - and pyproject_file.exists() - and tuple(original_addopts) not in {(), tuple(non_blacklist_plugin_args)} - ): - with Path.open(pyproject_file, "w", encoding="utf-8") as f: - f.write(original_content) + for file, (content, was_modified) in original_content.items(): + if was_modified: + with Path.open(file, "w", encoding="utf-8") as f: + f.write(content) @contextmanager From 333271c6ce63e1ea0878e3622034f4bd1522bb42 Mon Sep 17 00:00:00 2001 From: Aseem Saxena Date: Fri, 10 Oct 2025 18:24:46 -0700 Subject: [PATCH 03/10] wip --- codeflash/code_utils/code_utils.py | 94 ++++++++++++++++++++------- codeflash/code_utils/config_parser.py | 6 +- 2 files changed, 73 insertions(+), 27 deletions(-) diff --git a/codeflash/code_utils/code_utils.py b/codeflash/code_utils/code_utils.py index e264cf801..1f426db23 100644 --- a/codeflash/code_utils/code_utils.py +++ b/codeflash/code_utils/code_utils.py @@ -20,7 +20,7 @@ ImportErrorPattern = re.compile(r"ModuleNotFoundError.*$", re.MULTILINE) -BLACKLIST_ADDOPTS = ("benchmark", "sugar", "codespeed", "cov", "profile", "junitxml") +BLACKLIST_ADDOPTS = ("--benchmark", "--sugar", "--codespeed", "--cov", "--profile", "--junitxml", "-n") def unified_diff_strings(code1: str, code2: str, fromfile: str = "original", tofile: str = "modified") -> str: @@ -84,45 +84,93 @@ def create_rank_dictionary_compact(int_array: list[int]) -> dict[int, int]: return {original_index: rank for rank, original_index in enumerate(sorted_indices)} -def modify_addopts(config_file: Path) -> tuple[str, bool]: - content = "" +def filter_args(addopts_args: list[str]) -> list[str]: + filtered_args = [] + i = 0 + while i < len(addopts_args): + current_arg = addopts_args[i] + # Check if current argument starts with --cov + if current_arg.startswith(BLACKLIST_ADDOPTS): + # Skip this argument + i += 1 + # Check if the next argument is a value (doesn't start with -) + if i < len(addopts_args) and not addopts_args[i].startswith("-"): + # Skip the value as well + i += 1 + else: + # Keep this argument + filtered_args.append(current_arg) + i += 1 + return filtered_args + + +def modify_addopts(config_file: Path) -> tuple[str, bool]: # noqa : PLR0911 + file_type = config_file.suffix.lower() + filename = config_file.name + if file_type not in {".toml", ".ini", "cfg"} or not config_file.exists(): + return "", False + # Read original file + with Path.open(config_file, encoding="utf-8") as f: + content = f.read() try: - if config_file.suffix.lower() == "toml": + if filename == "pyproject.toml": # use tomlkit - pass + data = tomlkit.parse(content) + original_addopts = data.get("tool", {}).get("pytest", {}).get("ini_options", {}).get("addopts", "") + # nothing to do if no addopts present + if original_addopts == "": + return content, False + if isinstance(original_addopts, list): + original_addopts = " ".join(original_addopts) + addopts_args = ( + original_addopts.split() + ) # any number of space characters as delimiter, doesn't look at = which is fine else: # use configparser - pass + config = configparser.ConfigParser() + config.read_string(content) + data = {section: dict(config[section]) for section in config.sections()} + if config_file.name in {"pytest.ini", ".pytest.ini", "tox.ini"}: + original_addopts = data.get("pytest", {}).get("addopts", "") # should only be a string + else: + original_addopts = data.get("tool:pytest", {}).get("addopts", "") # should only be a string + addopts_args = original_addopts.split() + new_addopts_args = filter_args(addopts_args) + if new_addopts_args == addopts_args: + return content, False + # change addopts now + if file_type == "toml": + data["tool"]["pytest"]["ini_options"]["addopts"] = " ".join(new_addopts_args) + # Write modified file + with Path.open(config_file, "w", encoding="utf-8") as f: + f.write(tomlkit.dumps(data)) + return content, True + elif config_file.name in {"pytest.ini", ".pytest.ini", "tox.ini"}: + config["pytest"]["addopts"] = " ".join(new_addopts_args) + # Write modified file + with Path.open(config_file, "w", encoding="utf-8") as f: + config.write(f) + return content, True + else: + config["tool:pytest"]["addopts"] = " ".join(new_addopts_args) + # Write modified file + with Path.open(config_file, "w", encoding="utf-8") as f: + config.write(f) + return content, True + except Exception: logger.debug("Trouble parsing") return content, False # not modified - config = configparser.ConfigParser() - config.read(config_file) - # read file - # parse - # modify - # save - # return original content - print(config_file) - return "", True @contextmanager def custom_addopts() -> None: closest_config_files = get_all_closest_config_files() - # 1. find closest config files - # 2. iterate through each of them and mask the addopts - # 3. yield - # 4. restore the original addopts when the context manager exits - original_content = {} try: for config_file in closest_config_files: - # Read original file - print(config_file) - # if pyproject_file.exists(): original_content[config_file] = modify_addopts(config_file) yield diff --git a/codeflash/code_utils/config_parser.py b/codeflash/code_utils/config_parser.py index 7e877d03d..d322068cf 100644 --- a/codeflash/code_utils/config_parser.py +++ b/codeflash/code_utils/config_parser.py @@ -42,7 +42,7 @@ def get_all_closest_config_files() -> list[Path]: return all_closest_config_files -def find_closest_config_file(file_type: str) -> Path: +def find_closest_config_file(file_type: str) -> Path | None: # Find the closest pyproject.toml, pytest.ini, tox.ini, or setup.cfg file on the root of the project dir_path = Path.cwd() cur_path = dir_path @@ -57,9 +57,7 @@ def find_closest_config_file(file_type: str) -> Path: return config_file # Search for pyproject.toml in the parent directories dir_path = dir_path.parent - msg = f"Could not find pyproject.toml in the current directory {Path.cwd()} or any of the parent directories. Please create it by running `codeflash init`, or pass the path to pyproject.toml with the --config-file argument." - - raise ValueError(msg) + return None def find_conftest_files(test_paths: list[Path]) -> list[Path]: From 82f1b81596598b25c8645beace13ef470af760e8 Mon Sep 17 00:00:00 2001 From: Aseem Saxena Date: Fri, 10 Oct 2025 19:21:20 -0700 Subject: [PATCH 04/10] todo cleanup write tests --- codeflash/code_utils/code_utils.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/codeflash/code_utils/code_utils.py b/codeflash/code_utils/code_utils.py index 1f426db23..37e7a94cf 100644 --- a/codeflash/code_utils/code_utils.py +++ b/codeflash/code_utils/code_utils.py @@ -107,7 +107,8 @@ def filter_args(addopts_args: list[str]) -> list[str]: def modify_addopts(config_file: Path) -> tuple[str, bool]: # noqa : PLR0911 file_type = config_file.suffix.lower() filename = config_file.name - if file_type not in {".toml", ".ini", "cfg"} or not config_file.exists(): + config = None + if file_type not in {".toml", ".ini", ".cfg"} or not config_file.exists(): return "", False # Read original file with Path.open(config_file, encoding="utf-8") as f: @@ -146,13 +147,13 @@ def modify_addopts(config_file: Path) -> tuple[str, bool]: # noqa : PLR0911 f.write(tomlkit.dumps(data)) return content, True elif config_file.name in {"pytest.ini", ".pytest.ini", "tox.ini"}: - config["pytest"]["addopts"] = " ".join(new_addopts_args) + config.set("pytest", "addopts", " ".join(new_addopts_args)) # Write modified file with Path.open(config_file, "w", encoding="utf-8") as f: config.write(f) return content, True else: - config["tool:pytest"]["addopts"] = " ".join(new_addopts_args) + config.set("tool:pytest", "addopts", " ".join(new_addopts_args)) # Write modified file with Path.open(config_file, "w", encoding="utf-8") as f: config.write(f) From 79cee0d73dd2a1eb3a366fe503d6857504d312f0 Mon Sep 17 00:00:00 2001 From: Aseem Saxena Date: Fri, 10 Oct 2025 21:05:23 -0700 Subject: [PATCH 05/10] todo cleanup write tests --- codeflash/code_utils/code_utils.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/codeflash/code_utils/code_utils.py b/codeflash/code_utils/code_utils.py index 37e7a94cf..e104da07a 100644 --- a/codeflash/code_utils/code_utils.py +++ b/codeflash/code_utils/code_utils.py @@ -123,6 +123,7 @@ def modify_addopts(config_file: Path) -> tuple[str, bool]: # noqa : PLR0911 return content, False if isinstance(original_addopts, list): original_addopts = " ".join(original_addopts) + original_addopts = original_addopts.replace("=", " ") addopts_args = ( original_addopts.split() ) # any number of space characters as delimiter, doesn't look at = which is fine @@ -135,6 +136,7 @@ def modify_addopts(config_file: Path) -> tuple[str, bool]: # noqa : PLR0911 original_addopts = data.get("pytest", {}).get("addopts", "") # should only be a string else: original_addopts = data.get("tool:pytest", {}).get("addopts", "") # should only be a string + original_addopts = original_addopts.replace("=", " ") addopts_args = original_addopts.split() new_addopts_args = filter_args(addopts_args) if new_addopts_args == addopts_args: From da667fe88787495c93056f5018f022163437ecc6 Mon Sep 17 00:00:00 2001 From: Aseem Saxena Date: Fri, 10 Oct 2025 21:42:43 -0700 Subject: [PATCH 06/10] basic tests, write more, write for functions not being tested yet --- tests/code_utils/test_code_utils.py | 86 +++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 tests/code_utils/test_code_utils.py diff --git a/tests/code_utils/test_code_utils.py b/tests/code_utils/test_code_utils.py new file mode 100644 index 000000000..36f001670 --- /dev/null +++ b/tests/code_utils/test_code_utils.py @@ -0,0 +1,86 @@ +from __future__ import annotations + +import configparser +import os +from pathlib import Path +from unittest.mock import patch + +import tomlkit + +from codeflash.code_utils.code_utils import custom_addopts + + +def test_custom_addopts_modifies_and_restores_ini_file(tmp_path: Path) -> None: + """Verify that custom_addopts correctly modifies and then restores a pytest.ini file.""" + # Create a dummy pytest.ini file + config_file = tmp_path / "pytest.ini" + original_content = "[pytest]\naddopts = -v --cov=./src -n auto\n" + config_file.write_text(original_content) + + # Use patch to mock get_all_closest_config_files + with patch("codeflash.code_utils.code_utils.get_all_closest_config_files", return_value=[config_file]): + with custom_addopts(): + # Check that the file is modified inside the context + modified_content = config_file.read_text() + config = configparser.ConfigParser() + config.read_string(modified_content) + modified_addopts = config.get("pytest", "addopts", fallback="") + assert modified_addopts == "-v" + + # Check that the file is restored after exiting the context + restored_content = config_file.read_text() + assert restored_content.strip() == original_content.strip() + + +def test_custom_addopts_modifies_and_restores_toml_file(tmp_path: Path) -> None: + """Verify that custom_addopts correctly modifies and then restores a pyproject.toml file.""" + # Create a dummy pyproject.toml file + config_file = tmp_path / "pyproject.toml" + os.chdir(tmp_path) + original_addopts = "-v --cov=./src --junitxml=report.xml" + original_content_dict = { + "tool": {"pytest": {"ini_options": {"addopts": original_addopts}}} + } + original_content = tomlkit.dumps(original_content_dict) + config_file.write_text(original_content) + + # Use patch to mock get_all_closest_config_files + with patch("codeflash.code_utils.code_utils.get_all_closest_config_files", return_value=[config_file]): + with custom_addopts(): + # Check that the file is modified inside the context + modified_content = config_file.read_text() + modified_data = tomlkit.parse(modified_content) + modified_addopts = modified_data.get("tool", {}).get("pytest", {}).get("ini_options", {}).get("addopts", "") + assert modified_addopts == "-v" + + # Check that the file is restored after exiting the context + restored_content = config_file.read_text() + assert restored_content.strip() == original_content.strip() + + +def test_custom_addopts_handles_no_addopts(tmp_path: Path) -> None: + """Ensure custom_addopts doesn't fail when a config file has no addopts.""" + # Create a dummy pytest.ini file without addopts + config_file = tmp_path / "pytest.ini" + original_content = "[pytest]\n# no addopts here\n" + config_file.write_text(original_content) + + with patch("codeflash.code_utils.code_utils.get_all_closest_config_files", return_value=[config_file]): + with custom_addopts(): + # The file should not be modified + content_inside_context = config_file.read_text() + assert content_inside_context == original_content + + # The file should remain unchanged + content_after_context = config_file.read_text() + assert content_after_context == original_content + +def test_custom_addopts_handles_no_relevant_files(tmp_path: Path) -> None: + """Ensure custom_addopts runs without error when no config files are found.""" + # No config files created in tmp_path + + with patch("codeflash.code_utils.code_utils.get_all_closest_config_files", return_value=[]): + # This should execute without raising any exceptions + with custom_addopts(): + pass + # No assertions needed, the test passes if no exceptions were raised From 9048d38963a65d414d14a83c413077cbf6529771 Mon Sep 17 00:00:00 2001 From: Aseem Saxena Date: Fri, 10 Oct 2025 21:43:00 -0700 Subject: [PATCH 07/10] basic tests, write more, write for functions not being tested yet --- codeflash/code_utils/code_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codeflash/code_utils/code_utils.py b/codeflash/code_utils/code_utils.py index e104da07a..cf046f786 100644 --- a/codeflash/code_utils/code_utils.py +++ b/codeflash/code_utils/code_utils.py @@ -142,7 +142,7 @@ def modify_addopts(config_file: Path) -> tuple[str, bool]: # noqa : PLR0911 if new_addopts_args == addopts_args: return content, False # change addopts now - if file_type == "toml": + if file_type == ".toml": data["tool"]["pytest"]["ini_options"]["addopts"] = " ".join(new_addopts_args) # Write modified file with Path.open(config_file, "w", encoding="utf-8") as f: From 8b64b3903c9b24eb2be5c7404d142b95b5e455fb Mon Sep 17 00:00:00 2001 From: Aseem Saxena Date: Mon, 13 Oct 2025 18:23:00 -0700 Subject: [PATCH 08/10] remove patch and use actual function call --- tests/code_utils/test_code_utils.py | 68 +++++++++++++++++++---------- 1 file changed, 44 insertions(+), 24 deletions(-) diff --git a/tests/code_utils/test_code_utils.py b/tests/code_utils/test_code_utils.py index 36f001670..7bdc26f48 100644 --- a/tests/code_utils/test_code_utils.py +++ b/tests/code_utils/test_code_utils.py @@ -9,6 +9,26 @@ from codeflash.code_utils.code_utils import custom_addopts +def test_custom_addopts_modifies_and_restores_dotini_file(tmp_path: Path) -> None: + """Verify that custom_addopts correctly modifies and then restores a pytest.ini file.""" + # Create a dummy pytest.ini file + config_file = tmp_path / ".pytest.ini" + original_content = "[pytest]\naddopts = -v --cov=./src -n auto\n" + config_file.write_text(original_content) + + # Use patch to mock get_all_closest_config_files + os.chdir(tmp_path) + with custom_addopts(): + # Check that the file is modified inside the context + modified_content = config_file.read_text() + config = configparser.ConfigParser() + config.read_string(modified_content) + modified_addopts = config.get("pytest", "addopts", fallback="") + assert modified_addopts == "-v" + + # Check that the file is restored after exiting the context + restored_content = config_file.read_text() + assert restored_content.strip() == original_content.strip() def test_custom_addopts_modifies_and_restores_ini_file(tmp_path: Path) -> None: """Verify that custom_addopts correctly modifies and then restores a pytest.ini file.""" @@ -18,14 +38,14 @@ def test_custom_addopts_modifies_and_restores_ini_file(tmp_path: Path) -> None: config_file.write_text(original_content) # Use patch to mock get_all_closest_config_files - with patch("codeflash.code_utils.code_utils.get_all_closest_config_files", return_value=[config_file]): - with custom_addopts(): - # Check that the file is modified inside the context - modified_content = config_file.read_text() - config = configparser.ConfigParser() - config.read_string(modified_content) - modified_addopts = config.get("pytest", "addopts", fallback="") - assert modified_addopts == "-v" + os.chdir(tmp_path) + with custom_addopts(): + # Check that the file is modified inside the context + modified_content = config_file.read_text() + config = configparser.ConfigParser() + config.read_string(modified_content) + modified_addopts = config.get("pytest", "addopts", fallback="") + assert modified_addopts == "-v" # Check that the file is restored after exiting the context restored_content = config_file.read_text() @@ -45,13 +65,13 @@ def test_custom_addopts_modifies_and_restores_toml_file(tmp_path: Path) -> None: config_file.write_text(original_content) # Use patch to mock get_all_closest_config_files - with patch("codeflash.code_utils.code_utils.get_all_closest_config_files", return_value=[config_file]): - with custom_addopts(): - # Check that the file is modified inside the context - modified_content = config_file.read_text() - modified_data = tomlkit.parse(modified_content) - modified_addopts = modified_data.get("tool", {}).get("pytest", {}).get("ini_options", {}).get("addopts", "") - assert modified_addopts == "-v" + os.chdir(tmp_path) + with custom_addopts(): + # Check that the file is modified inside the context + modified_content = config_file.read_text() + modified_data = tomlkit.parse(modified_content) + modified_addopts = modified_data.get("tool", {}).get("pytest", {}).get("ini_options", {}).get("addopts", "") + assert modified_addopts == "-v" # Check that the file is restored after exiting the context restored_content = config_file.read_text() @@ -65,11 +85,11 @@ def test_custom_addopts_handles_no_addopts(tmp_path: Path) -> None: original_content = "[pytest]\n# no addopts here\n" config_file.write_text(original_content) - with patch("codeflash.code_utils.code_utils.get_all_closest_config_files", return_value=[config_file]): - with custom_addopts(): - # The file should not be modified - content_inside_context = config_file.read_text() - assert content_inside_context == original_content + os.chdir(tmp_path) + with custom_addopts(): + # The file should not be modified + content_inside_context = config_file.read_text() + assert content_inside_context == original_content # The file should remain unchanged content_after_context = config_file.read_text() @@ -79,8 +99,8 @@ def test_custom_addopts_handles_no_relevant_files(tmp_path: Path) -> None: """Ensure custom_addopts runs without error when no config files are found.""" # No config files created in tmp_path - with patch("codeflash.code_utils.code_utils.get_all_closest_config_files", return_value=[]): - # This should execute without raising any exceptions - with custom_addopts(): - pass + os.chdir(tmp_path) + # This should execute without raising any exceptions + with custom_addopts(): + pass # No assertions needed, the test passes if no exceptions were raised From 7d9ee86b3a10e364f47bef098a83c9d316d5527a Mon Sep 17 00:00:00 2001 From: Aseem Saxena Date: Mon, 13 Oct 2025 18:28:06 -0700 Subject: [PATCH 09/10] tests done, ready to review --- tests/code_utils/test_code_utils.py | 84 +++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/tests/code_utils/test_code_utils.py b/tests/code_utils/test_code_utils.py index 7bdc26f48..73a8e8b2f 100644 --- a/tests/code_utils/test_code_utils.py +++ b/tests/code_utils/test_code_utils.py @@ -2,9 +2,11 @@ import configparser import os +import stat from pathlib import Path from unittest.mock import patch +import pytest import tomlkit from codeflash.code_utils.code_utils import custom_addopts @@ -104,3 +106,85 @@ def test_custom_addopts_handles_no_relevant_files(tmp_path: Path) -> None: with custom_addopts(): pass # No assertions needed, the test passes if no exceptions were raised + + +def test_custom_addopts_toml_without_pytest_section(tmp_path: Path) -> None: + """Verify custom_addopts doesn't fail with a toml file missing a [tool.pytest] section.""" + config_file = tmp_path / "pyproject.toml" + original_content_dict = {"tool": {"other_tool": {"key": "value"}}} + original_content = tomlkit.dumps(original_content_dict) + config_file.write_text(original_content) + + os.chdir(tmp_path) + with custom_addopts(): + content_inside_context = config_file.read_text() + assert content_inside_context == original_content + + content_after_context = config_file.read_text() + assert content_after_context == original_content + + +def test_custom_addopts_ini_without_pytest_section(tmp_path: Path) -> None: + """Verify custom_addopts doesn't fail with an ini file missing a [pytest] section.""" + config_file = tmp_path / "pytest.ini" + original_content = "[other_section]\nkey = value\n" + config_file.write_text(original_content) + + os.chdir(tmp_path) + with custom_addopts(): + content_inside_context = config_file.read_text() + assert content_inside_context == original_content + + content_after_context = config_file.read_text() + assert content_after_context == original_content + + +def test_custom_addopts_with_multiple_config_files(tmp_path: Path) -> None: + """Verify custom_addopts modifies and restores all found config files.""" + os.chdir(tmp_path) + + # Create pytest.ini + ini_file = tmp_path / "pytest.ini" + ini_original_content = "[pytest]\naddopts = -v --cov\n" + ini_file.write_text(ini_original_content) + + # Create pyproject.toml + toml_file = tmp_path / "pyproject.toml" + toml_original_addopts = "-s -n auto" + toml_original_content_dict = { + "tool": {"pytest": {"ini_options": {"addopts": toml_original_addopts}}} + } + toml_original_content = tomlkit.dumps(toml_original_content_dict) + toml_file.write_text(toml_original_content) + + with custom_addopts(): + # Check INI file modification + ini_modified_content = ini_file.read_text() + config = configparser.ConfigParser() + config.read_string(ini_modified_content) + assert config.get("pytest", "addopts", fallback="") == "-v" + + # Check TOML file modification + toml_modified_content = toml_file.read_text() + modified_data = tomlkit.parse(toml_modified_content) + modified_addopts = modified_data.get("tool", {}).get("pytest", {}).get("ini_options", {}).get("addopts", "") + assert modified_addopts == "-s" + + # Check that both files are restored + assert ini_file.read_text().strip() == ini_original_content.strip() + assert toml_file.read_text().strip() == toml_original_content.strip() + + +def test_custom_addopts_restores_on_exception(tmp_path: Path) -> None: + """Ensure config file is restored even if an exception occurs inside the context.""" + config_file = tmp_path / "pytest.ini" + original_content = "[pytest]\naddopts = -v --cov\n" + config_file.write_text(original_content) + + os.chdir(tmp_path) + with pytest.raises(ValueError, match="Test exception"): + with custom_addopts(): + raise ValueError("Test exception") + + restored_content = config_file.read_text() + assert restored_content.strip() == original_content.strip() From 4414b250aa4034b203a4f946ea37aef48aa5456e Mon Sep 17 00:00:00 2001 From: Aseem Saxena Date: Mon, 13 Oct 2025 18:35:38 -0700 Subject: [PATCH 10/10] Update codeflash/code_utils/code_utils.py Co-authored-by: codeflash-ai[bot] <148906541+codeflash-ai[bot]@users.noreply.github.com> --- codeflash/code_utils/code_utils.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/codeflash/code_utils/code_utils.py b/codeflash/code_utils/code_utils.py index cf046f786..1bc4d66bb 100644 --- a/codeflash/code_utils/code_utils.py +++ b/codeflash/code_utils/code_utils.py @@ -85,20 +85,20 @@ def create_rank_dictionary_compact(int_array: list[int]) -> dict[int, int]: def filter_args(addopts_args: list[str]) -> list[str]: + # Convert BLACKLIST_ADDOPTS to a set for faster lookup of simple matches + # But keep tuple for startswith + blacklist = BLACKLIST_ADDOPTS + # Precompute the length for re-use + n = len(addopts_args) filtered_args = [] i = 0 - while i < len(addopts_args): + while i < n: current_arg = addopts_args[i] - # Check if current argument starts with --cov - if current_arg.startswith(BLACKLIST_ADDOPTS): - # Skip this argument + if current_arg.startswith(blacklist): i += 1 - # Check if the next argument is a value (doesn't start with -) - if i < len(addopts_args) and not addopts_args[i].startswith("-"): - # Skip the value as well + if i < n and not addopts_args[i].startswith("-"): i += 1 else: - # Keep this argument filtered_args.append(current_arg) i += 1 return filtered_args