diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a9656fc1..df9148c4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,6 +37,15 @@ jobs: with: python-version: ${{ env.min-python-version }} + - name: Set Date + run: echo "DATE=$(date +'%Y-%m-%d')" >> $GITHUB_ENV + + - name: Cache Python packages + uses: actions/cache@v3 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}-${{ env.DATE }} + - name: Install dependencies run: | python -m pip install --upgrade pip @@ -124,6 +133,15 @@ jobs: with: python-version: ${{ matrix.python-version }} + - name: Set Date + run: echo "DATE=$(date +'%Y-%m-%d')" >> $GITHUB_ENV + + - name: Cache Python packages + uses: actions/cache@v3 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}-${{ env.DATE }} + - name: Install dependencies run: | python -m pip install --upgrade pip diff --git a/autogpt/commands/file_operations.py b/autogpt/commands/file_operations.py index 824db50c..cb5fb36c 100644 --- a/autogpt/commands/file_operations.py +++ b/autogpt/commands/file_operations.py @@ -4,6 +4,7 @@ from __future__ import annotations import hashlib import os import os.path +import re from typing import TYPE_CHECKING, Generator, Literal import requests @@ -224,6 +225,68 @@ def write_to_file(filename: str, text: str, config: Config) -> str: return f"Error: {err}" +@command( + "replace_in_file", + "Replace text or code in a file", + '"filename": "", ' + '"old_text": "", "new_text": "", ' + '"occurrence_index": ""', +) +def replace_in_file( + filename: str, old_text: str, new_text: str, config: Config, occurrence_index=None +): + """Update a file by replacing one or all occurrences of old_text with new_text using Python's built-in string + manipulation and regular expression modules for cross-platform file editing similar to sed and awk. + + Args: + filename (str): The name of the file + old_text (str): String to be replaced. \n will be stripped from the end. + new_text (str): New string. \n will be stripped from the end. + occurrence_index (int): Optional index of the occurrence to replace. If None, all occurrences will be replaced. + + Returns: + str: A message indicating whether the file was updated successfully or if there were no matches found for old_text + in the file. + + Raises: + Exception: If there was an error updating the file. + """ + try: + with open(filename, "r", encoding="utf-8") as f: + content = f.read() + + old_text = old_text.rstrip("\n") + new_text = new_text.rstrip("\n") + + if occurrence_index is None: + new_content = content.replace(old_text, new_text) + else: + matches = list(re.finditer(re.escape(old_text), content)) + if not matches: + return f"No matches found for {old_text} in {filename}" + + if int(occurrence_index) >= len(matches): + return f"Occurrence index {occurrence_index} is out of range for {old_text} in {filename}" + + match = matches[int(occurrence_index)] + start, end = match.start(), match.end() + new_content = content[:start] + new_text + content[end:] + + if content == new_content: + return f"No matches found for {old_text} in {filename}" + + with open(filename, "w", encoding="utf-8") as f: + f.write(new_content) + + with open(filename, "r", encoding="utf-8") as f: + checksum = text_checksum(f.read()) + log_operation("update", filename, config, checksum=checksum) + + return f"File {filename} updated successfully." + except Exception as e: + return "Error: " + str(e) + + @command( "append_to_file", "Append to file", '"filename": "", "text": ""' ) diff --git a/tests/Auto-GPT-test-cassettes b/tests/Auto-GPT-test-cassettes index be280df4..28497a8b 160000 --- a/tests/Auto-GPT-test-cassettes +++ b/tests/Auto-GPT-test-cassettes @@ -1 +1 @@ -Subproject commit be280df43d6a23b8074d9cba10d18ed8724a54c9 +Subproject commit 28497a8b4ef0e62375dd5024794426cb401a3779 diff --git a/tests/integration/challenges/current_score.json b/tests/integration/challenges/current_score.json index deb4a82d..6a133f3e 100644 --- a/tests/integration/challenges/current_score.json +++ b/tests/integration/challenges/current_score.json @@ -12,7 +12,7 @@ "debug_code": { "debug_code_challenge_a": { "max_level": 1, - "max_level_beaten": 1 + "max_level_beaten": null } }, "information_retrieval": { diff --git a/tests/unit/test_file_operations.py b/tests/unit/test_file_operations.py index 35c77a15..1d0219eb 100644 --- a/tests/unit/test_file_operations.py +++ b/tests/unit/test_file_operations.py @@ -268,6 +268,53 @@ def test_write_file_succeeds_if_content_different( assert result == "File written to successfully." +# Update file testing +def test_replace_in_file_all_occurrences(test_file, test_file_path, config): + old_content = "This is a test file.\n we test file here\na test is needed" + expected_content = ( + "This is a update file.\n we update file here\na update is needed" + ) + test_file.write(old_content) + test_file.close() + file_ops.replace_in_file(test_file_path, "test", "update", config) + with open(test_file_path) as f: + new_content = f.read() + print(new_content) + print(expected_content) + assert new_content == expected_content + + +def test_replace_in_file_one_occurrence(test_file, test_file_path, config): + old_content = "This is a test file.\n we test file here\na test is needed" + expected_content = "This is a test file.\n we update file here\na test is needed" + test_file.write(old_content) + test_file.close() + file_ops.replace_in_file( + test_file_path, "test", "update", config, occurrence_index=1 + ) + with open(test_file_path) as f: + new_content = f.read() + + assert new_content == expected_content + + +def test_replace_in_file_multiline_old_text(test_file, test_file_path, config): + old_content = "This is a multi_line\ntest for testing\nhow well this function\nworks when the input\nis multi-lined" + expected_content = "This is a multi_line\nfile. succeeded test\nis multi-lined" + test_file.write(old_content) + test_file.close() + file_ops.replace_in_file( + test_file_path, + "\ntest for testing\nhow well this function\nworks when the input\n", + "\nfile. succeeded test\n", + config, + ) + with open(test_file_path) as f: + new_content = f.read() + + assert new_content == expected_content + + def test_append_to_file(test_nested_file: Path, config): append_text = "This is appended text.\n" file_ops.write_to_file(test_nested_file, append_text, config)