mlcflow 1.2.7__tar.gz → 1.2.9__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.
- {mlcflow-1.2.7 → mlcflow-1.2.9}/PKG-INFO +1 -1
- {mlcflow-1.2.7 → mlcflow-1.2.9}/mlc/action.py +8 -3
- {mlcflow-1.2.7 → mlcflow-1.2.9}/mlc/repo_action.py +29 -3
- {mlcflow-1.2.7 → mlcflow-1.2.9}/mlcflow.egg-info/PKG-INFO +1 -1
- {mlcflow-1.2.7 → mlcflow-1.2.9}/mlcflow.egg-info/SOURCES.txt +3 -1
- {mlcflow-1.2.7 → mlcflow-1.2.9}/pyproject.toml +1 -1
- mlcflow-1.2.9/tests/test_mlcflow_unix_installer.py +137 -0
- mlcflow-1.2.9/tests/test_repo_action_extra_git_args.py +20 -0
- mlcflow-1.2.9/tests/test_unix_installer_venv.py +224 -0
- mlcflow-1.2.7/tests/test_mlcflow_unix_installer.py +0 -83
- {mlcflow-1.2.7 → mlcflow-1.2.9}/LICENSE.md +0 -0
- {mlcflow-1.2.7 → mlcflow-1.2.9}/README.md +0 -0
- {mlcflow-1.2.7 → mlcflow-1.2.9}/mlc/__init__.py +0 -0
- {mlcflow-1.2.7 → mlcflow-1.2.9}/mlc/__main__.py +0 -0
- {mlcflow-1.2.7 → mlcflow-1.2.9}/mlc/action_factory.py +0 -0
- {mlcflow-1.2.7 → mlcflow-1.2.9}/mlc/cache_action.py +0 -0
- {mlcflow-1.2.7 → mlcflow-1.2.9}/mlc/cfg_action.py +0 -0
- {mlcflow-1.2.7 → mlcflow-1.2.9}/mlc/error_codes.py +0 -0
- {mlcflow-1.2.7 → mlcflow-1.2.9}/mlc/experiment_action.py +0 -0
- {mlcflow-1.2.7 → mlcflow-1.2.9}/mlc/index.py +0 -0
- {mlcflow-1.2.7 → mlcflow-1.2.9}/mlc/item.py +0 -0
- {mlcflow-1.2.7 → mlcflow-1.2.9}/mlc/logger.py +0 -0
- {mlcflow-1.2.7 → mlcflow-1.2.9}/mlc/main.py +0 -0
- {mlcflow-1.2.7 → mlcflow-1.2.9}/mlc/meta_schema.py +0 -0
- {mlcflow-1.2.7 → mlcflow-1.2.9}/mlc/repo.py +0 -0
- {mlcflow-1.2.7 → mlcflow-1.2.9}/mlc/script_action.py +0 -0
- {mlcflow-1.2.7 → mlcflow-1.2.9}/mlc/utils.py +0 -0
- {mlcflow-1.2.7 → mlcflow-1.2.9}/mlcflow.egg-info/dependency_links.txt +0 -0
- {mlcflow-1.2.7 → mlcflow-1.2.9}/mlcflow.egg-info/entry_points.txt +0 -0
- {mlcflow-1.2.7 → mlcflow-1.2.9}/mlcflow.egg-info/requires.txt +0 -0
- {mlcflow-1.2.7 → mlcflow-1.2.9}/mlcflow.egg-info/top_level.txt +0 -0
- {mlcflow-1.2.7 → mlcflow-1.2.9}/setup.cfg +0 -0
- {mlcflow-1.2.7 → mlcflow-1.2.9}/tests/test_action_invalid_meta_entries.py +0 -0
- {mlcflow-1.2.7 → mlcflow-1.2.9}/tests/test_cache_mark_tmp.py +0 -0
- {mlcflow-1.2.7 → mlcflow-1.2.9}/tests/test_script_action_apptainer.py +0 -0
|
@@ -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', not 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
|
-
|
|
508
|
-
|
|
509
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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):
|
|
@@ -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/
|
|
30
|
+
tests/test_repo_action_extra_git_args.py
|
|
31
|
+
tests/test_script_action_apptainer.py
|
|
32
|
+
tests/test_unix_installer_venv.py
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|