/ tests / projects / test_virtualenv_projects.py
test_virtualenv_projects.py
  1  import os
  2  from unittest import mock
  3  
  4  import pytest
  5  
  6  import mlflow
  7  from mlflow.exceptions import MlflowException
  8  from mlflow.utils.virtualenv import _create_virtualenv
  9  from mlflow.utils.yaml_utils import read_yaml, write_yaml
 10  
 11  from tests.projects.utils import (
 12      TEST_VIRTUALENV_CONDA_PROJECT_DIR,
 13      TEST_VIRTUALENV_NO_PYTHON_ENV,
 14      TEST_VIRTUALENV_PROJECT_DIR,
 15  )
 16  
 17  spy_on_create_virtualenv = mock.patch(
 18      "mlflow.projects.backend.local._create_virtualenv", wraps=_create_virtualenv
 19  )
 20  
 21  
 22  @pytest.fixture(autouse=True, scope="module")
 23  def use_dev_mlflow_for_projects():
 24      mlflow_root = os.path.dirname(os.path.dirname(mlflow.__file__))
 25  
 26      conda_env = read_yaml(TEST_VIRTUALENV_CONDA_PROJECT_DIR, "conda.yaml")
 27      conda_pip_dependencies = next(
 28          item for item in conda_env["dependencies"] if isinstance(item, dict) and "pip" in item
 29      )["pip"]
 30      if "mlflow" in conda_pip_dependencies:
 31          conda_pip_dependencies.remove("mlflow")
 32          conda_pip_dependencies.append(mlflow_root)
 33      write_yaml(TEST_VIRTUALENV_CONDA_PROJECT_DIR, "conda.yaml", conda_env, overwrite=True)
 34  
 35      for proj_dir in (TEST_VIRTUALENV_PROJECT_DIR, TEST_VIRTUALENV_NO_PYTHON_ENV):
 36          virtualenv_requirements_path = os.path.join(proj_dir, "requirements.txt")
 37          with open(virtualenv_requirements_path) as f:
 38              virtualenv_requirements = f.readlines()
 39  
 40          with open(virtualenv_requirements_path, "w") as f:
 41              for line in virtualenv_requirements:
 42                  if line.rstrip("\n") != "mlflow":
 43                      f.write(line)
 44                  else:
 45                      f.write(mlflow_root)
 46                      f.write("\n")
 47  
 48  
 49  @spy_on_create_virtualenv
 50  def test_virtualenv_project_execution_virtualenv(create_virtualenv_spy):
 51      submitted_run = mlflow.projects.run(
 52          TEST_VIRTUALENV_PROJECT_DIR, entry_point="test", env_manager="virtualenv"
 53      )
 54      submitted_run.wait()
 55      create_virtualenv_spy.assert_called_once()
 56  
 57  
 58  @spy_on_create_virtualenv
 59  def test_virtualenv_project_execution_uv(create_virtualenv_spy):
 60      submitted_run = mlflow.projects.run(
 61          TEST_VIRTUALENV_PROJECT_DIR, entry_point="test", env_manager="uv"
 62      )
 63      submitted_run.wait()
 64      create_virtualenv_spy.assert_called_once()
 65  
 66  
 67  @spy_on_create_virtualenv
 68  def test_virtualenv_project_execution_without_env_manager(create_virtualenv_spy):
 69      # python_env project should be executed using virtualenv without explicitly specifying
 70      # env_manager="virtualenv"
 71      submitted_run = mlflow.projects.run(TEST_VIRTUALENV_PROJECT_DIR, entry_point="test")
 72      submitted_run.wait()
 73      create_virtualenv_spy.assert_called_once()
 74  
 75  
 76  @spy_on_create_virtualenv
 77  def test_virtualenv_project_execution_no_python_env(create_virtualenv_spy):
 78      """
 79      When an MLproject file doesn't contain a `python_env` key but python_env.yaml exists,
 80      virtualenv should be used as an environment manager.
 81      """
 82      submitted_run = mlflow.projects.run(TEST_VIRTUALENV_NO_PYTHON_ENV, entry_point="test")
 83      submitted_run.wait()
 84      create_virtualenv_spy.assert_called_once()
 85  
 86  
 87  @spy_on_create_virtualenv
 88  def test_virtualenv_project_execution_local(create_virtualenv_spy):
 89      submitted_run = mlflow.projects.run(
 90          TEST_VIRTUALENV_PROJECT_DIR, entry_point="main", env_manager="local"
 91      )
 92      submitted_run.wait()
 93      create_virtualenv_spy.assert_not_called()
 94  
 95  
 96  @spy_on_create_virtualenv
 97  def test_virtualenv_conda_project_execution(create_virtualenv_spy):
 98      submitted_run = mlflow.projects.run(
 99          TEST_VIRTUALENV_CONDA_PROJECT_DIR, entry_point="test", env_manager="virtualenv"
100      )
101      submitted_run.wait()
102      create_virtualenv_spy.assert_called_once()
103  
104  
105  def test_virtualenv_project_execution_conda():
106      with pytest.raises(MlflowException, match="python_env project cannot be executed using conda"):
107          mlflow.projects.run(TEST_VIRTUALENV_PROJECT_DIR, env_manager="conda")
108  
109  
110  @spy_on_create_virtualenv
111  def test_virtualenv_project_no_env_file(create_virtualenv_spy, tmp_path):
112      """
113      When neither python_env.yaml nor conda.yaml is present, virtualenv should be used as an
114      environment manager.
115      """
116      ml_project_file = tmp_path.joinpath("MLproject")
117      ml_project_file.write_text(
118          """
119  name: test
120  entry_points:
121    main:
122      command: |
123        python test.py
124  """
125      )
126      tmp_path.joinpath("test.py").write_text(
127          """
128  import os
129  
130  assert "VIRTUAL_ENV" in os.environ
131  """
132      )
133      mlflow.projects.run(str(tmp_path))
134      create_virtualenv_spy.assert_called_once()
135  
136  
137  @spy_on_create_virtualenv
138  def test_virtualenv_project_no_mlmodel_file(create_virtualenv_spy, tmp_path):
139      tmp_path.joinpath("test.py").write_text(
140          """
141  import os
142  
143  assert "VIRTUAL_ENV" in os.environ
144  """
145      )
146      mlflow.projects.run(str(tmp_path), entry_point="test.py")
147      create_virtualenv_spy.assert_called_once()