Files
Zamil Majdyandmajdyz f2dbfa7e7c [AGPT-621] Moving on with code generation with a buggy code (Ruff - PyRight failure) (#181)
To prevent frustrated tester, we should let buggy code to be generated instead of failing the project generation (if it failed on the linter) by appending a # TODO line on the code.

---------

Co-authored-by: majdyz <[email protected]>
2024-04-04 17:38:06 +02:00

173 lines
5.8 KiB
Python

import asyncio
import enum
import logging
import os
import subprocess
import tempfile
from asyncio.subprocess import Process
from codex.common.ai_block import ValidationError, ValidationErrorWithContent
logger = logging.getLogger(__name__)
class OutputType(enum.Enum):
STD_OUT = "stdout"
STD_ERR = "stderr"
BOTH = "both"
async def exec_external_on_contents(
command_arguments: list[str],
file_contents,
suffix: str = ".py",
output_type: OutputType = OutputType.BOTH,
raise_file_contents_on_error: bool = False,
) -> str:
"""
Execute an external tool with the provided command arguments and file contents
:param command_arguments: The command arguments to execute
:param file_contents: The file contents to execute the command on
:param suffix: The suffix of the temporary file. Default is ".py"
:return: The file contents after the command has been executed
Note: The file contents are written to a temporary file and the command is executed
on that file. The command arguments should be a list of strings, where the first
element is the command to execute and the rest of the elements are the arguments to
the command. There is no need to provide the file path as an argument, as it will
be appended to the command arguments.
Example:
exec_external(["ruff", "check"], "print('Hello World')")
will run the command "ruff check <temp_file_path>" with the file contents
"print('Hello World')" and return the file contents after the command
has been executed.
"""
errors = ""
if len(command_arguments) == 0:
raise AssertionError("No command arguments provided")
# Run ruff to validate the code
with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as temp_file:
temp_file_path = temp_file.name
temp_file.write(file_contents.encode("utf-8"))
temp_file.flush()
command_arguments.append(str(temp_file_path))
# Run Ruff on the temporary file
try:
r: Process = await asyncio.create_subprocess_exec(
*command_arguments,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
result = await r.communicate()
stdout, stderr = result[0].decode("utf-8"), result[1].decode("utf-8")
logger.debug(f"Output: {stdout}")
if temp_file_path in stdout:
stdout = stdout # .replace(temp_file.name, "/generated_file")
logger.debug(f"Errors: {stderr}")
if output_type == OutputType.STD_OUT:
errors = stdout
elif output_type == OutputType.STD_ERR:
errors = stderr
else:
errors = stdout + "\n" + stderr
with open(temp_file_path, "r") as f:
file_contents = f.read()
finally:
# Ensure the temporary file is deleted
os.remove(temp_file_path)
if not errors:
return file_contents
if raise_file_contents_on_error:
raise ValidationErrorWithContent(errors, file_contents)
raise ValidationError(errors)
PROJECT_TEMP_DIR = os.path.join(tempfile.gettempdir(), "codex-static-code-analysis")
DEFAULT_DEPS = ["prisma", "pyright", "pydantic", "virtualenv-clone"]
async def setup_if_required(cwd: str, copy_from_parent: bool = False) -> str:
"""
Setup the virtual environment if it does not exist
This setup is executed expectedly once per application run
Args:
cwd (str): The current working directory
copy_from_parent (bool): Whether to copy the virtual environment from the parent directory
Returns:
str: The path to the virtual environment
"""
if not os.path.exists(cwd):
os.makedirs(cwd, exist_ok=True)
path = f"{cwd}/venv/bin"
if os.path.exists(path):
return path
parent_dir = os.path.abspath(f"{cwd}/../")
if copy_from_parent and os.path.exists(f"{parent_dir}/venv"):
parent_path = await setup_if_required(parent_dir)
await execute_command(
["virtualenv-clone", f"{parent_dir}/venv", f"{cwd}/venv"], cwd, parent_path
)
return path
# Create a virtual environment
output = await execute_command(["python", "-m", "venv", "venv"], cwd, None)
logger.info(f"[Setup] Creating virtual environment: {output}")
# Install dependencies
output = await execute_command(["pip", "install"] + DEFAULT_DEPS, cwd, path)
logger.info(f"[Setup] Installing {DEFAULT_DEPS}: {output}")
output = await execute_command(["pyright"], cwd, path, raise_on_error=False)
logger.info(f"[Setup] Setting up pyright: {output}")
return path
async def execute_command(
command: list[str],
cwd: str | None,
python_path: str | None = None,
raise_on_error: bool = True,
) -> str:
"""
Execute a command in the shell
Args:
command (list[str]): The command to execute
cwd (str): The current working directory
python_path (str): The python executable path
raise_on_error (bool): Whether to raise an error if the command fails
Returns:
str: The output of the command
"""
# Set the python path by replacing the env 'PATH' with the provided python path
venv = os.environ.copy()
if python_path:
# PATH prioritize first occurrence of python_path, so we need to prepend.
venv["PATH"] = python_path + ":" + venv["PATH"]
r = await asyncio.create_subprocess_exec(
*command,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
cwd=cwd,
env=venv,
)
stdout, stderr = await r.communicate()
if r.returncode == 0:
return (stdout or stderr).decode("utf-8")
if raise_on_error:
raise ValidationError((stderr or stdout).decode("utf-8"))
else:
return (stderr or stdout).decode("utf-8")