mlcflow 1.2.7__tar.gz → 1.2.8__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. {mlcflow-1.2.7 → mlcflow-1.2.8}/PKG-INFO +1 -1
  2. {mlcflow-1.2.7 → mlcflow-1.2.8}/mlc/action.py +8 -3
  3. {mlcflow-1.2.7 → mlcflow-1.2.8}/mlc/repo_action.py +29 -3
  4. {mlcflow-1.2.7 → mlcflow-1.2.8}/mlcflow.egg-info/PKG-INFO +1 -1
  5. {mlcflow-1.2.7 → mlcflow-1.2.8}/mlcflow.egg-info/SOURCES.txt +3 -1
  6. {mlcflow-1.2.7 → mlcflow-1.2.8}/pyproject.toml +1 -1
  7. mlcflow-1.2.8/tests/test_mlcflow_unix_installer.py +137 -0
  8. mlcflow-1.2.8/tests/test_repo_action_extra_git_args.py +20 -0
  9. mlcflow-1.2.8/tests/test_unix_installer_venv.py +224 -0
  10. mlcflow-1.2.7/tests/test_mlcflow_unix_installer.py +0 -83
  11. {mlcflow-1.2.7 → mlcflow-1.2.8}/LICENSE.md +0 -0
  12. {mlcflow-1.2.7 → mlcflow-1.2.8}/README.md +0 -0
  13. {mlcflow-1.2.7 → mlcflow-1.2.8}/mlc/__init__.py +0 -0
  14. {mlcflow-1.2.7 → mlcflow-1.2.8}/mlc/__main__.py +0 -0
  15. {mlcflow-1.2.7 → mlcflow-1.2.8}/mlc/action_factory.py +0 -0
  16. {mlcflow-1.2.7 → mlcflow-1.2.8}/mlc/cache_action.py +0 -0
  17. {mlcflow-1.2.7 → mlcflow-1.2.8}/mlc/cfg_action.py +0 -0
  18. {mlcflow-1.2.7 → mlcflow-1.2.8}/mlc/error_codes.py +0 -0
  19. {mlcflow-1.2.7 → mlcflow-1.2.8}/mlc/experiment_action.py +0 -0
  20. {mlcflow-1.2.7 → mlcflow-1.2.8}/mlc/index.py +0 -0
  21. {mlcflow-1.2.7 → mlcflow-1.2.8}/mlc/item.py +0 -0
  22. {mlcflow-1.2.7 → mlcflow-1.2.8}/mlc/logger.py +0 -0
  23. {mlcflow-1.2.7 → mlcflow-1.2.8}/mlc/main.py +0 -0
  24. {mlcflow-1.2.7 → mlcflow-1.2.8}/mlc/meta_schema.py +0 -0
  25. {mlcflow-1.2.7 → mlcflow-1.2.8}/mlc/repo.py +0 -0
  26. {mlcflow-1.2.7 → mlcflow-1.2.8}/mlc/script_action.py +0 -0
  27. {mlcflow-1.2.7 → mlcflow-1.2.8}/mlc/utils.py +0 -0
  28. {mlcflow-1.2.7 → mlcflow-1.2.8}/mlcflow.egg-info/dependency_links.txt +0 -0
  29. {mlcflow-1.2.7 → mlcflow-1.2.8}/mlcflow.egg-info/entry_points.txt +0 -0
  30. {mlcflow-1.2.7 → mlcflow-1.2.8}/mlcflow.egg-info/requires.txt +0 -0
  31. {mlcflow-1.2.7 → mlcflow-1.2.8}/mlcflow.egg-info/top_level.txt +0 -0
  32. {mlcflow-1.2.7 → mlcflow-1.2.8}/setup.cfg +0 -0
  33. {mlcflow-1.2.7 → mlcflow-1.2.8}/tests/test_action_invalid_meta_entries.py +0 -0
  34. {mlcflow-1.2.7 → mlcflow-1.2.8}/tests/test_cache_mark_tmp.py +0 -0
  35. {mlcflow-1.2.7 → mlcflow-1.2.8}/tests/test_script_action_apptainer.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mlcflow
3
- Version: 1.2.7
3
+ Version: 1.2.8
4
4
  Summary: An automation interface tailored for CPU/GPU benchmarking
5
5
  Author-email: MLCommons <systems@mlcommons.org>
6
6
  License:
@@ -5,6 +5,7 @@ import yaml
5
5
  import logging
6
6
  import re
7
7
  import shutil
8
+ import sys
8
9
  from pathlib import Path
9
10
 
10
11
  from .logger import logger, setup_logging
@@ -483,6 +484,7 @@ class Action:
483
484
  target_name = i.get('target_name', i.get('target', "cache"))
484
485
  i['target_name'] = target_name
485
486
  ii = i.copy()
487
+ quiet = ii.get('quiet', sys.stdin.isatty())
486
488
 
487
489
  if i.get('search_tags'):
488
490
  ii['tags'] = ",".join(i['search_tags'])
@@ -504,9 +506,12 @@ class Action:
504
506
 
505
507
  new_tags = set(search_tags)
506
508
  if len(found_items) > 1:
507
- # Step 3: Ask user for confirmation if multiple items are found
508
- user_input = input(
509
- f"{len(found_items)} items found. Do you want to update all? (yes/no): ").strip().lower()
509
+ if quiet:
510
+ user_input = 'yes'
511
+ else:
512
+ # Step 3: Ask user for confirmation if multiple items are found
513
+ user_input = input(
514
+ f"{len(found_items)} items found. Do you want to update all? (yes/no): ").strip().lower()
510
515
  if user_input not in ['yes', 'y']:
511
516
  return {'return': 0,
512
517
  'message': 'Update operation canceled by the user.'}
@@ -40,6 +40,8 @@ class RepoAction(Action):
40
40
 
41
41
  """
42
42
 
43
+ # These phrases come from git's current stderr output and may vary by
44
+ # version or locale, so keep the matching logic narrowly scoped.
43
45
  GIT_MISSING_BRANCH_PHRASE = "did not match any file(s) known to git"
44
46
  GIT_FAST_FORWARD_FAILURE_PHRASE = "not possible to fast-forward"
45
47
 
@@ -183,9 +185,19 @@ class RepoAction(Action):
183
185
 
184
186
  @staticmethod
185
187
  def _subprocess_error_message(error):
186
- """Return the most helpful message from a subprocess-related error."""
188
+ """Return stderr, then stdout, then str(error), whichever is populated first."""
187
189
  return (error.stderr or error.stdout or str(error)).strip()
188
190
 
191
+ @staticmethod
192
+ def _validate_extra_git_args(extra_args):
193
+ disallowed_pattern = re.compile(r"[\r\n]")
194
+ for arg in extra_args:
195
+ if disallowed_pattern.search(arg):
196
+ return (
197
+ "--extra_git_args may not include carriage returns or newlines."
198
+ )
199
+ return None
200
+
189
201
  def add(self, run_args):
190
202
  """
191
203
  ####################################################################################################################
@@ -521,7 +533,7 @@ class RepoAction(Action):
521
533
  clone_depth = int(depth)
522
534
  except (TypeError, ValueError):
523
535
  return {
524
- "return": 1, "error": f"Invalid value for --depth: {depth!r}. Must be a positive integer."}
536
+ "return": 1, "error": f"Invalid value for --depth: {depth}. Must be a positive integer."}
525
537
  if clone_depth < 1:
526
538
  return {
527
539
  "return": 1, "error": f"Invalid value for --depth: {clone_depth}. Must be a positive integer."}
@@ -532,9 +544,23 @@ class RepoAction(Action):
532
544
  extra_args = []
533
545
  if extra_git_args:
534
546
  if isinstance(extra_git_args, list):
547
+ if not all(isinstance(arg, str) for arg in extra_git_args):
548
+ return {
549
+ "return": 1,
550
+ "error": "--extra_git_args list entries must all be strings."
551
+ }
535
552
  extra_args = extra_git_args
553
+ elif isinstance(extra_git_args, str):
554
+ extra_args = shlex.split(extra_git_args)
536
555
  else:
537
- extra_args = shlex.split(str(extra_git_args))
556
+ return {
557
+ "return": 1,
558
+ "error": "--extra_git_args must be provided as a string or list of strings."
559
+ }
560
+ extra_git_args_error = self._validate_extra_git_args(
561
+ extra_args)
562
+ if extra_git_args_error:
563
+ return {"return": 1, "error": extra_git_args_error}
538
564
 
539
565
  # If the directory doesn't exist, clone it
540
566
  if not os.path.exists(repo_path):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mlcflow
3
- Version: 1.2.7
3
+ Version: 1.2.8
4
4
  Summary: An automation interface tailored for CPU/GPU benchmarking
5
5
  Author-email: MLCommons <systems@mlcommons.org>
6
6
  License:
@@ -27,4 +27,6 @@ mlcflow.egg-info/top_level.txt
27
27
  tests/test_action_invalid_meta_entries.py
28
28
  tests/test_cache_mark_tmp.py
29
29
  tests/test_mlcflow_unix_installer.py
30
- tests/test_script_action_apptainer.py
30
+ tests/test_repo_action_extra_git_args.py
31
+ tests/test_script_action_apptainer.py
32
+ tests/test_unix_installer_venv.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "mlcflow"
7
- version = "1.2.7"
7
+ version = "1.2.8"
8
8
 
9
9
  description = "An automation interface tailored for CPU/GPU benchmarking"
10
10
  authors = [
@@ -0,0 +1,137 @@
1
+ import platform
2
+ import subprocess
3
+ import sys
4
+ import tempfile
5
+ import textwrap
6
+ from pathlib import Path
7
+
8
+
9
+ REPO_ROOT = Path(__file__).resolve().parents[1]
10
+ INSTALLER_SCRIPT = REPO_ROOT / "docs" / "install" / "mlcflow_unix_installer.sh"
11
+
12
+
13
+ def _resolve_venv_dir(tmp_path, setup_snippet):
14
+ default_dir = tmp_path / "mlcflow"
15
+ marker = "__RESULT__"
16
+ command = textwrap.dedent(
17
+ f"""
18
+ set -euo pipefail
19
+ source "{INSTALLER_SCRIPT}"
20
+ DEFAULT_VENV_DIR="{default_dir}"
21
+ VENV_DIR="$DEFAULT_VENV_DIR"
22
+ PY_MAJOR_MINOR="$(python3 -c 'import sys; print("{{}}.{{}}".format(*sys.version_info[:2]))')"
23
+ PY_ARCH="$(python3 -c 'import platform; print(platform.machine())')"
24
+ {setup_snippet}
25
+ resolve_default_venv_dir
26
+ echo "{marker}$VENV_DIR"
27
+ """
28
+ )
29
+ result = subprocess.run(
30
+ ["bash", "-lc", command],
31
+ text=True,
32
+ capture_output=True,
33
+ check=True,
34
+ )
35
+
36
+ for line in reversed(result.stdout.splitlines()):
37
+ if line.startswith(marker):
38
+ return line[len(marker):]
39
+
40
+ raise AssertionError(
41
+ f"Could not parse resolved venv path from output:\n{result.stdout}"
42
+ )
43
+
44
+
45
+ def test_resolve_default_venv_dir_prefers_compatible_default(tmp_path):
46
+ setup_snippet = """
47
+ python3 -m venv "$DEFAULT_VENV_DIR"
48
+ """
49
+ resolved = _resolve_venv_dir(tmp_path, setup_snippet)
50
+ assert resolved == str(tmp_path / "mlcflow")
51
+
52
+
53
+ def test_resolve_default_venv_dir_uses_suffix_for_incompatible_default(
54
+ tmp_path):
55
+ setup_snippet = """
56
+ mkdir -p "$DEFAULT_VENV_DIR"
57
+ """
58
+ resolved = _resolve_venv_dir(tmp_path, setup_snippet)
59
+ assert resolved.startswith(str(tmp_path / "mlcflow_"))
60
+ assert "_py" in resolved
61
+
62
+
63
+ def test_resolve_default_venv_dir_removes_stale_shared_env_when_default_incompatible(
64
+ tmp_path,
65
+ ):
66
+ """Broken suffixed envs should be removed so setup_venv recreates them."""
67
+ setup_snippet = """
68
+ mkdir -p "$DEFAULT_VENV_DIR"
69
+ SHARED_VENV_DIR="${DEFAULT_VENV_DIR}_${PY_ARCH}_py${PY_MAJOR_MINOR}"
70
+ mkdir -p "$SHARED_VENV_DIR"
71
+ """
72
+ resolved = _resolve_venv_dir(tmp_path, setup_snippet)
73
+ expected_suffix = (
74
+ f"{tmp_path / 'mlcflow'}_{platform.machine()}"
75
+ f"_py{sys.version_info[0]}.{sys.version_info[1]}"
76
+ )
77
+ assert resolved == expected_suffix
78
+ assert not Path(expected_suffix).exists()
79
+
80
+
81
+ def test_resolve_default_venv_dir_reuses_compatible_suffixed_env(tmp_path):
82
+ setup_snippet = """
83
+ SHARED_VENV_DIR="${DEFAULT_VENV_DIR}_${PY_ARCH}_py${PY_MAJOR_MINOR}"
84
+ python3 -m venv "$SHARED_VENV_DIR"
85
+ """
86
+ resolved = _resolve_venv_dir(tmp_path, setup_snippet)
87
+ expected = (
88
+ f"{tmp_path / 'mlcflow'}_{platform.machine()}"
89
+ f"_py{sys.version_info[0]}.{sys.version_info[1]}"
90
+ )
91
+ assert resolved == expected
92
+
93
+
94
+ def test_get_venv_suffix_and_compatibility_signature_use_consistent_python_values():
95
+ with tempfile.TemporaryDirectory() as temp_dir:
96
+ shim_path = Path(temp_dir) / "python-shim"
97
+ shim_path.write_text(
98
+ textwrap.dedent(
99
+ """\
100
+ #!/usr/bin/env python3
101
+ import os
102
+ import platform
103
+ import sys
104
+
105
+ machine = os.environ["FAKE_MACHINE"]
106
+ major = int(os.environ.get("FAKE_PY_MAJOR", "3"))
107
+ minor = int(os.environ.get("FAKE_PY_MINOR", "12"))
108
+ code = sys.argv[2]
109
+
110
+ platform.machine = lambda: machine
111
+ sys.version_info = (major, minor, 0, "final", 0)
112
+ exec(code, {"__name__": "__main__"})
113
+ """
114
+ ),
115
+ encoding="utf-8",
116
+ )
117
+ shim_path.chmod(0o755)
118
+
119
+ command = textwrap.dedent(
120
+ f"""
121
+ set -euo pipefail
122
+ source "{INSTALLER_SCRIPT}"
123
+ PYTHON_CMD="{shim_path}"
124
+ export FAKE_MACHINE=arm64
125
+ export FAKE_PY_MAJOR=3
126
+ export FAKE_PY_MINOR=12
127
+ printf '__RESULT__:%s:%s\\n' "$(get_venv_suffix)" "$(get_python_compatibility_signature "$PYTHON_CMD")"
128
+ """
129
+ )
130
+ result = subprocess.run(
131
+ ["bash", "-lc", command],
132
+ text=True,
133
+ capture_output=True,
134
+ check=True,
135
+ )
136
+
137
+ assert "__RESULT__:_arm64_py3.12:arm64|3.12" in result.stdout
@@ -0,0 +1,20 @@
1
+ from mlc.repo_action import RepoAction
2
+
3
+
4
+ def test_validate_extra_git_args_allows_literal_shell_characters():
5
+ assert RepoAction._validate_extra_git_args(
6
+ [
7
+ "refs/pull/$PR/head",
8
+ "http.extraHeader=Authorization: ******",
9
+ "name-with|pipe",
10
+ "`literal-backticks`",
11
+ ]
12
+ ) is None
13
+
14
+
15
+ def test_validate_extra_git_args_rejects_line_breaks():
16
+ error = RepoAction._validate_extra_git_args(["line1\nline2"])
17
+
18
+ assert error == (
19
+ "--extra_git_args may not include carriage returns or newlines."
20
+ )
@@ -0,0 +1,224 @@
1
+ import os
2
+ import platform
3
+ import shlex
4
+ import subprocess
5
+ import sys
6
+ import tempfile
7
+ import textwrap
8
+ import unittest
9
+
10
+
11
+ REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
12
+ INSTALLER_PATH = os.path.join(
13
+ REPO_ROOT,
14
+ "docs",
15
+ "install",
16
+ "mlcflow_unix_installer.sh")
17
+ # Keep this in sync with docs/install/mlcflow_unix_installer.sh:
18
+ # get_venv_suffix() and get_python_compatibility_signature().
19
+ COMPATIBILITY_SIGNATURE_CODE = (
20
+ 'import platform, sys; '
21
+ 'print("{}|{}.{}".format(platform.machine(), sys.version_info[0], sys.version_info[1]))'
22
+ )
23
+
24
+
25
+ class UnixInstallerVenvTest(unittest.TestCase):
26
+ def setUp(self):
27
+ self.temp_dir = tempfile.TemporaryDirectory()
28
+ self.addCleanup(self.temp_dir.cleanup)
29
+
30
+ def _expected_suffix(self):
31
+ return (
32
+ f"_{platform.machine()}"
33
+ f"_py{sys.version_info[0]}.{sys.version_info[1]}"
34
+ )
35
+
36
+ def _run_setup_venv(self, venv_dir):
37
+ command = """
38
+ set -euo pipefail
39
+ source {installer}
40
+ VENV_DIR={venv_dir}
41
+ setup_venv
42
+ printf '__RESULT__:%s:%s\\n' "$VENV_DIR" "${{VIRTUAL_ENV:-}}"
43
+ """.format(
44
+ installer=shlex.quote(INSTALLER_PATH),
45
+ venv_dir=shlex.quote(venv_dir),
46
+ )
47
+
48
+ completed = subprocess.run(
49
+ ["bash", "-lc", command],
50
+ cwd=self.temp_dir.name,
51
+ capture_output=True,
52
+ text=True,
53
+ check=False,
54
+ )
55
+ self.assertEqual(completed.returncode, 0, msg=completed.stderr)
56
+
57
+ result_lines = [
58
+ line for line in completed.stdout.splitlines()
59
+ if line.startswith("__RESULT__:")
60
+ ]
61
+ self.assertTrue(result_lines, msg=completed.stdout)
62
+ _, resolved_path, activated_path = result_lines[-1].split(":", 2)
63
+ return resolved_path, activated_path, completed.stdout
64
+
65
+ def _compatibility_signature(self, python_executable):
66
+ completed = subprocess.run(
67
+ [
68
+ python_executable,
69
+ "-c",
70
+ COMPATIBILITY_SIGNATURE_CODE,
71
+ ],
72
+ capture_output=True,
73
+ text=True,
74
+ check=True,
75
+ )
76
+ return completed.stdout.strip()
77
+
78
+ def _write_installer_with_stubbed_main_dependencies(self):
79
+ integration_installer_path = os.path.join(
80
+ self.temp_dir.name, "mlcflow_unix_installer_integration_test.sh"
81
+ )
82
+ stubbed_functions = textwrap.dedent("""
83
+ detect_os() {
84
+ OS_ID="ubuntu"
85
+ OS_VERSION="test"
86
+ PKG_MANAGER="apt"
87
+ }
88
+
89
+ check_missing_dependencies() {
90
+ MISSING_DEPS=()
91
+ }
92
+
93
+ ensure_python() {
94
+ :
95
+ }
96
+
97
+ install_mlcflow() {
98
+ :
99
+ }
100
+
101
+ prompt_repo_details() {
102
+ :
103
+ }
104
+
105
+ pull_repo() {
106
+ :
107
+ }
108
+ """).strip()
109
+ with open(INSTALLER_PATH, "r", encoding="utf-8") as installer_file:
110
+ installer_contents = installer_file.read().rstrip()
111
+ main_guard = textwrap.dedent(
112
+ """
113
+ if [[ "${BASH_SOURCE[0]:-$0}" == "$0" ]]; then
114
+ main
115
+ fi
116
+ """
117
+ ).strip()
118
+ replacement = (
119
+ stubbed_functions
120
+ + "\n\n"
121
+ + main_guard
122
+ )
123
+ self.assertIn(main_guard, installer_contents)
124
+ modified_contents = installer_contents.replace(main_guard, replacement)
125
+ self.assertIn(replacement, modified_contents)
126
+ with open(integration_installer_path, "w", encoding="utf-8") as installer_file:
127
+ installer_file.write(modified_contents)
128
+ return integration_installer_path
129
+
130
+ def test_installer_main_runs_setup_venv_with_stubbed_dependencies(self):
131
+ venv_dir = os.path.join(self.temp_dir.name, "mlcflow")
132
+ os.makedirs(venv_dir, exist_ok=True)
133
+ expected_path = venv_dir + self._expected_suffix()
134
+ installer_path = self._write_installer_with_stubbed_main_dependencies()
135
+
136
+ completed = subprocess.run(
137
+ ["bash", installer_path, "--yes", "--venv-dir", venv_dir],
138
+ cwd=self.temp_dir.name,
139
+ capture_output=True,
140
+ text=True,
141
+ check=False,
142
+ )
143
+
144
+ self.assertEqual(completed.returncode, 0, msg=completed.stderr)
145
+ self.assertIn("Installation completed successfully.", completed.stdout)
146
+ self.assertIn(expected_path, completed.stdout)
147
+ self.assertTrue(
148
+ os.path.exists(
149
+ os.path.join(
150
+ expected_path,
151
+ "bin",
152
+ "python")))
153
+ self.assertEqual(
154
+ self._compatibility_signature(
155
+ os.path.join(expected_path, "bin", "python")),
156
+ self._compatibility_signature(sys.executable),
157
+ )
158
+
159
+ def test_setup_venv_creates_requested_path_from_scratch(self):
160
+ venv_dir = os.path.join(self.temp_dir.name, "mlcflow")
161
+
162
+ resolved_path, activated_path, stdout = self._run_setup_venv(venv_dir)
163
+
164
+ self.assertEqual(resolved_path, venv_dir)
165
+ self.assertEqual(activated_path, venv_dir)
166
+ self.assertIn("Setting up virtual environment at", stdout)
167
+ self.assertTrue(
168
+ os.path.exists(
169
+ os.path.join(
170
+ venv_dir,
171
+ "bin",
172
+ "python")))
173
+ self.assertEqual(
174
+ self._compatibility_signature(
175
+ os.path.join(venv_dir, "bin", "python")),
176
+ self._compatibility_signature(sys.executable),
177
+ )
178
+
179
+ def test_setup_venv_reuses_compatible_existing_venv(self):
180
+ venv_dir = os.path.join(self.temp_dir.name, "mlcflow")
181
+ subprocess.run(
182
+ [sys.executable, "-m", "venv", venv_dir],
183
+ check=True,
184
+ cwd=self.temp_dir.name,
185
+ )
186
+
187
+ resolved_path, activated_path, stdout = self._run_setup_venv(venv_dir)
188
+
189
+ self.assertEqual(resolved_path, venv_dir)
190
+ self.assertEqual(activated_path, venv_dir)
191
+ self.assertIn("Reusing existing virtual environment.", stdout)
192
+ self.assertEqual(
193
+ self._compatibility_signature(
194
+ os.path.join(venv_dir, "bin", "python")),
195
+ self._compatibility_signature(sys.executable),
196
+ )
197
+
198
+ def test_setup_venv_uses_arch_and_python_suffix_for_incompatible_dir(self):
199
+ venv_dir = os.path.join(self.temp_dir.name, "mlcflow")
200
+ os.makedirs(venv_dir, exist_ok=True)
201
+ expected_path = venv_dir + self._expected_suffix()
202
+
203
+ resolved_path, activated_path, stdout = self._run_setup_venv(venv_dir)
204
+
205
+ self.assertEqual(resolved_path, expected_path)
206
+ self.assertEqual(activated_path, expected_path)
207
+ self.assertIn(
208
+ "is incompatible with current Python/platform. Using",
209
+ stdout)
210
+ self.assertTrue(
211
+ os.path.exists(
212
+ os.path.join(
213
+ expected_path,
214
+ "bin",
215
+ "python")))
216
+ self.assertEqual(
217
+ self._compatibility_signature(
218
+ os.path.join(expected_path, "bin", "python")),
219
+ self._compatibility_signature(sys.executable),
220
+ )
221
+
222
+
223
+ if __name__ == "__main__":
224
+ unittest.main()
@@ -1,83 +0,0 @@
1
- import subprocess
2
- import platform
3
- import sys
4
- from pathlib import Path
5
-
6
-
7
- REPO_ROOT = Path(__file__).resolve().parents[1]
8
- INSTALLER_SCRIPT = REPO_ROOT / "docs" / "install" / "mlcflow_unix_installer.sh"
9
-
10
-
11
- def _resolve_venv_dir(tmp_path, setup_snippet):
12
- default_dir = tmp_path / "mlcflow"
13
- marker = "__RESULT__"
14
- command = f"""
15
- set -euo pipefail
16
- source "{INSTALLER_SCRIPT}"
17
- DEFAULT_VENV_DIR="{default_dir}"
18
- VENV_DIR="$DEFAULT_VENV_DIR"
19
- PY_MAJOR_MINOR="$(python3 -c 'import sys; print("{{}}.{{}}".format(*sys.version_info[:2]))')"
20
- PY_ARCH="$(python3 -c 'import platform; print(platform.machine())')"
21
- {setup_snippet}
22
- resolve_default_venv_dir
23
- echo "{marker}$VENV_DIR"
24
- """
25
- result = subprocess.run(
26
- ["bash", "-lc", command],
27
- text=True,
28
- capture_output=True,
29
- check=True,
30
- )
31
-
32
- for line in reversed(result.stdout.splitlines()):
33
- if line.startswith(marker):
34
- return line[len(marker):]
35
-
36
- raise AssertionError(
37
- f"Could not parse resolved venv path from output:\n{result.stdout}")
38
-
39
-
40
- def test_resolve_default_venv_dir_prefers_compatible_default(tmp_path):
41
- setup_snippet = """
42
- python3 -m venv "$DEFAULT_VENV_DIR"
43
- """
44
- resolved = _resolve_venv_dir(tmp_path, setup_snippet)
45
- assert resolved == str(tmp_path / "mlcflow")
46
-
47
-
48
- def test_resolve_default_venv_dir_uses_suffix_for_incompatible_default(
49
- tmp_path):
50
- setup_snippet = """
51
- mkdir -p "$DEFAULT_VENV_DIR"
52
- """
53
- resolved = _resolve_venv_dir(tmp_path, setup_snippet)
54
- assert resolved.startswith(str(tmp_path / "mlcflow_"))
55
- assert "_py" in resolved
56
-
57
-
58
- def test_resolve_default_venv_dir_removes_stale_shared_env_when_default_incompatible(
59
- tmp_path):
60
- """When the default venv is incompatible and the suffixed venv already exists
61
- but is itself stale/broken, the stale suffixed dir must be removed so that
62
- setup_venv can create a fresh one rather than silently reusing a broken env."""
63
- setup_snippet = """
64
- mkdir -p "$DEFAULT_VENV_DIR"
65
- SHARED_VENV_DIR="${DEFAULT_VENV_DIR}_${PY_ARCH}_py${PY_MAJOR_MINOR}"
66
- mkdir -p "$SHARED_VENV_DIR"
67
- """
68
- resolved = _resolve_venv_dir(tmp_path, setup_snippet)
69
- expected_suffix = f"{tmp_path / 'mlcflow'}_{platform.machine()}_py{sys.version_info[0]}.{sys.version_info[1]}"
70
- assert resolved == expected_suffix
71
- # The stale shared dir should have been removed so setup_venv creates it
72
- # fresh
73
- assert not Path(expected_suffix).exists()
74
-
75
-
76
- def test_resolve_default_venv_dir_reuses_compatible_suffixed_env(tmp_path):
77
- setup_snippet = """
78
- SHARED_VENV_DIR="${DEFAULT_VENV_DIR}_${PY_ARCH}_py${PY_MAJOR_MINOR}"
79
- python3 -m venv "$SHARED_VENV_DIR"
80
- """
81
- resolved = _resolve_venv_dir(tmp_path, setup_snippet)
82
- expected = f"{tmp_path / 'mlcflow'}_{platform.machine()}_py{sys.version_info[0]}.{sys.version_info[1]}"
83
- assert resolved == expected
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes