"""
Module containing the virtual environment class.
"""
import logging
import os
import shutil
import grader.utils.constants as const
from grader.utils.logger import VERBOSE
from grader.utils.process import run
logger = logging.getLogger("grader")
[docs]
class VirtualEnvironment:
"""
Class that handles the creation and deletion of a virtual environment.
Acts as a context manager. Everything executed within it, can assume that the venv is setup.
"""
is_initialized = False
def __init__(self, project_path: str):
self._project_path = project_path
self._venv_path = os.path.join(project_path, const.VENV_NAME)
def __enter__(self):
self.setup()
VirtualEnvironment.is_initialized = True
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.teardown()
VirtualEnvironment.is_initialized = False
[docs]
def setup(self):
"""
Setup the virtual environment.
Check if there is an existing venv, if so, delete it.
Check if there is a requirements.txt file.
Create a new venv and install the requirements.
Install the grader dependencies as well.
"""
# Check for existing venv
possible_venv_paths = [os.path.join(self._project_path, venv_path) for venv_path in const.POSSIBLE_VENV_DIRS]
for path in possible_venv_paths:
if os.path.exists(path):
logger.log(VERBOSE, "Found existing venv at %s", path)
shutil.rmtree(path)
# Check for requirements.txt
requirements_path = os.path.join(self._project_path, const.REQUIREMENTS_FILENAME)
does_requirements_exist = os.path.exists(requirements_path)
if not does_requirements_exist:
logger.error("No requirements.txt file found in the project directory")
# Create new venv
logger.log(VERBOSE, "Creating new venv")
create_venv_result = run([const.PYTHON_BIN, "-m", "venv", self._venv_path])
if create_venv_result.returncode != 0:
logger.error("Failed to create virtual environment")
raise VirtualEnvironmentError("Failed to create virtual environment")
# Install requirements
if does_requirements_exist:
logger.log(VERBOSE, "Installing requirements")
VirtualEnvironment.__install_requirements(self._venv_path, requirements_path)
# Install grader dependencies
logger.log(VERBOSE, "Installing grader dependencies")
grader_requirements_path = const.GRADER_REQUIREMENTS
VirtualEnvironment.__install_requirements(self._venv_path, grader_requirements_path)
[docs]
def teardown(self):
"""
Delete the virtual environment.
"""
shutil.rmtree(self._venv_path)
@staticmethod
def __install_requirements(venv_path: str, requirements_path: str):
"""
Install the requirements specified in the requirements file into the virtual environment.
:param venv_path: The path to the virtual environment.
:type venv_path: str
:param requirements_path: The path to the requirements file.
:type requirements_path: str
:raises VirtualEnvironmentError: If the installation of requirements fails.
:return: None
:rtype: None
"""
pip_path = os.path.join(venv_path, const.PIP_PATH)
output = run([pip_path, "install", "-r", requirements_path])
if output.returncode != 0:
logger.error("Failed to install requirements from %s", requirements_path)
raise VirtualEnvironmentError(f"Failed to install requirements from {requirements_path}")
[docs]
class VirtualEnvironmentError(Exception):
"""
Exception raised when an error occurs during the virtual environment setup.
"""