mlcflow 1.2.5__tar.gz → 1.2.7__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.5 → mlcflow-1.2.7}/PKG-INFO +1 -1
- {mlcflow-1.2.5 → mlcflow-1.2.7}/mlc/main.py +2 -2
- {mlcflow-1.2.5 → mlcflow-1.2.7}/mlc/repo_action.py +249 -23
- {mlcflow-1.2.5 → mlcflow-1.2.7}/mlc/script_action.py +2 -1
- {mlcflow-1.2.5 → mlcflow-1.2.7}/mlcflow.egg-info/PKG-INFO +1 -1
- {mlcflow-1.2.5 → mlcflow-1.2.7}/mlcflow.egg-info/SOURCES.txt +1 -0
- {mlcflow-1.2.5 → mlcflow-1.2.7}/pyproject.toml +1 -1
- mlcflow-1.2.7/tests/test_mlcflow_unix_installer.py +83 -0
- mlcflow-1.2.7/tests/test_script_action_apptainer.py +54 -0
- mlcflow-1.2.5/tests/test_script_action_apptainer.py +0 -29
- {mlcflow-1.2.5 → mlcflow-1.2.7}/LICENSE.md +0 -0
- {mlcflow-1.2.5 → mlcflow-1.2.7}/README.md +0 -0
- {mlcflow-1.2.5 → mlcflow-1.2.7}/mlc/__init__.py +0 -0
- {mlcflow-1.2.5 → mlcflow-1.2.7}/mlc/__main__.py +0 -0
- {mlcflow-1.2.5 → mlcflow-1.2.7}/mlc/action.py +0 -0
- {mlcflow-1.2.5 → mlcflow-1.2.7}/mlc/action_factory.py +0 -0
- {mlcflow-1.2.5 → mlcflow-1.2.7}/mlc/cache_action.py +0 -0
- {mlcflow-1.2.5 → mlcflow-1.2.7}/mlc/cfg_action.py +0 -0
- {mlcflow-1.2.5 → mlcflow-1.2.7}/mlc/error_codes.py +0 -0
- {mlcflow-1.2.5 → mlcflow-1.2.7}/mlc/experiment_action.py +0 -0
- {mlcflow-1.2.5 → mlcflow-1.2.7}/mlc/index.py +0 -0
- {mlcflow-1.2.5 → mlcflow-1.2.7}/mlc/item.py +0 -0
- {mlcflow-1.2.5 → mlcflow-1.2.7}/mlc/logger.py +0 -0
- {mlcflow-1.2.5 → mlcflow-1.2.7}/mlc/meta_schema.py +0 -0
- {mlcflow-1.2.5 → mlcflow-1.2.7}/mlc/repo.py +0 -0
- {mlcflow-1.2.5 → mlcflow-1.2.7}/mlc/utils.py +0 -0
- {mlcflow-1.2.5 → mlcflow-1.2.7}/mlcflow.egg-info/dependency_links.txt +0 -0
- {mlcflow-1.2.5 → mlcflow-1.2.7}/mlcflow.egg-info/entry_points.txt +0 -0
- {mlcflow-1.2.5 → mlcflow-1.2.7}/mlcflow.egg-info/requires.txt +0 -0
- {mlcflow-1.2.5 → mlcflow-1.2.7}/mlcflow.egg-info/top_level.txt +0 -0
- {mlcflow-1.2.5 → mlcflow-1.2.7}/setup.cfg +0 -0
- {mlcflow-1.2.5 → mlcflow-1.2.7}/tests/test_action_invalid_meta_entries.py +0 -0
- {mlcflow-1.2.5 → mlcflow-1.2.7}/tests/test_cache_mark_tmp.py +0 -0
|
@@ -576,7 +576,7 @@ def main():
|
|
|
576
576
|
else:
|
|
577
577
|
pre_args.target, pre_args.action = pre_args.action, None
|
|
578
578
|
actions = get_action(pre_args.target, default_parent)
|
|
579
|
-
help_text += actions.__doc__
|
|
579
|
+
help_text += actions.__doc__ or ""
|
|
580
580
|
# iterate through every method
|
|
581
581
|
for method_name, method in inspect.getmembers(
|
|
582
582
|
actions.__class__, inspect.isfunction):
|
|
@@ -588,7 +588,7 @@ def main():
|
|
|
588
588
|
action_name = pre_args.action.replace("-", "_")
|
|
589
589
|
try:
|
|
590
590
|
method = getattr(actions, action_name)
|
|
591
|
-
help_text += actions.__doc__
|
|
591
|
+
help_text += actions.__doc__ or ""
|
|
592
592
|
if method.__doc__:
|
|
593
593
|
help_text += method.__doc__
|
|
594
594
|
except AttributeError:
|
|
@@ -2,6 +2,7 @@ from .action import Action
|
|
|
2
2
|
import os
|
|
3
3
|
import subprocess
|
|
4
4
|
import re
|
|
5
|
+
import shlex
|
|
5
6
|
import yaml
|
|
6
7
|
import json
|
|
7
8
|
import shutil
|
|
@@ -39,11 +40,152 @@ class RepoAction(Action):
|
|
|
39
40
|
|
|
40
41
|
"""
|
|
41
42
|
|
|
43
|
+
GIT_MISSING_BRANCH_PHRASE = "did not match any file(s) known to git"
|
|
44
|
+
GIT_FAST_FORWARD_FAILURE_PHRASE = "not possible to fast-forward"
|
|
45
|
+
|
|
42
46
|
def __init__(self, parent=None):
|
|
43
47
|
# super().__init__(parent)
|
|
44
48
|
self.parent = parent
|
|
45
49
|
self.__dict__.update(vars(parent))
|
|
46
50
|
|
|
51
|
+
def _build_pull_command(self, repo_path, branch=None, clone_depth=None,
|
|
52
|
+
fast_forward_only=False):
|
|
53
|
+
pull_command = ['git', '-C', repo_path, 'pull']
|
|
54
|
+
if fast_forward_only:
|
|
55
|
+
pull_command.append('--ff-only')
|
|
56
|
+
if clone_depth is not None:
|
|
57
|
+
if self._is_shallow_repo(repo_path):
|
|
58
|
+
pull_command.extend(['--depth', str(clone_depth)])
|
|
59
|
+
else:
|
|
60
|
+
logger.warning(
|
|
61
|
+
f"--depth/--shallow ignored for pull on non-shallow repo {repo_path}. "
|
|
62
|
+
"Re-clone with --shallow to create a shallow copy."
|
|
63
|
+
)
|
|
64
|
+
# Preserve the historical existing-repo behavior of a plain
|
|
65
|
+
# `git pull` by default. The branch argument is only forwarded to
|
|
66
|
+
# `git pull` when the internal strict path explicitly opts into
|
|
67
|
+
# fast-forward-only updates.
|
|
68
|
+
if branch and fast_forward_only:
|
|
69
|
+
pull_command.extend(['origin', branch])
|
|
70
|
+
return pull_command
|
|
71
|
+
|
|
72
|
+
def _checkout_pull_branch(self, repo_path, branch):
|
|
73
|
+
"""Ensure *branch* is checked out locally before a strict pull.
|
|
74
|
+
|
|
75
|
+
First tries to checkout an existing local branch. If that fails, fetches
|
|
76
|
+
the branch from origin and creates a new tracking branch. Raises
|
|
77
|
+
RuntimeError with contextual guidance when the branch cannot be prepared.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
repo_path: Local repository path.
|
|
81
|
+
branch: Branch name to prepare before pulling.
|
|
82
|
+
|
|
83
|
+
Raises:
|
|
84
|
+
RuntimeError: If the branch cannot be fetched or checked out.
|
|
85
|
+
"""
|
|
86
|
+
try:
|
|
87
|
+
subprocess.run(
|
|
88
|
+
['git', '-C', repo_path, 'checkout', branch],
|
|
89
|
+
capture_output=True,
|
|
90
|
+
text=True,
|
|
91
|
+
check=True)
|
|
92
|
+
except subprocess.CalledProcessError as checkout_error:
|
|
93
|
+
checkout_error_text = self._subprocess_error_message(
|
|
94
|
+
checkout_error)
|
|
95
|
+
lowered_checkout_error = checkout_error_text.lower()
|
|
96
|
+
# Git uses the same exit code for many checkout failures, so we
|
|
97
|
+
# only fall back to fetch+track when stderr includes both
|
|
98
|
+
# "pathspec" and Git's usual missing-ref phrase
|
|
99
|
+
# "did not match any file(s) known to git".
|
|
100
|
+
missing_local_branch = (
|
|
101
|
+
"pathspec" in lowered_checkout_error
|
|
102
|
+
and self.GIT_MISSING_BRANCH_PHRASE in lowered_checkout_error
|
|
103
|
+
)
|
|
104
|
+
if not missing_local_branch:
|
|
105
|
+
raise RuntimeError(
|
|
106
|
+
f"Cannot switch to branch '{branch}' in {repo_path}: "
|
|
107
|
+
f"{checkout_error_text}"
|
|
108
|
+
) from checkout_error
|
|
109
|
+
try:
|
|
110
|
+
subprocess.run(
|
|
111
|
+
['git', '-C', repo_path, 'fetch', 'origin', branch],
|
|
112
|
+
capture_output=True,
|
|
113
|
+
text=True,
|
|
114
|
+
check=True)
|
|
115
|
+
except subprocess.CalledProcessError as fetch_error:
|
|
116
|
+
raise RuntimeError(
|
|
117
|
+
f"Failed to fetch branch '{branch}' in {repo_path}: "
|
|
118
|
+
f"{self._subprocess_error_message(fetch_error)}. "
|
|
119
|
+
"Ensure the branch exists on origin and that the repository is reachable."
|
|
120
|
+
) from fetch_error
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
subprocess.run(
|
|
124
|
+
['git', '-C', repo_path, 'checkout', '-b',
|
|
125
|
+
branch, '--track', f'origin/{branch}'],
|
|
126
|
+
capture_output=True,
|
|
127
|
+
text=True,
|
|
128
|
+
check=True)
|
|
129
|
+
except subprocess.CalledProcessError as tracking_error:
|
|
130
|
+
raise RuntimeError(
|
|
131
|
+
f"Initial checkout of '{branch}' failed in {repo_path}. "
|
|
132
|
+
"After fetching from origin, creating a tracking branch also failed. "
|
|
133
|
+
f"Initial error: {checkout_error_text}. "
|
|
134
|
+
f"Tracking branch error: {self._subprocess_error_message(tracking_error)}. "
|
|
135
|
+
"Check that the branch name is correct and that your local checkout can track origin."
|
|
136
|
+
) from tracking_error
|
|
137
|
+
|
|
138
|
+
@staticmethod
|
|
139
|
+
def _format_stash_restore_guidance(repo_path):
|
|
140
|
+
return (
|
|
141
|
+
f"Local changes remain in stash. Please run `git -C {repo_path} stash apply` "
|
|
142
|
+
"after resolving pull issues."
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
def _format_pull_error(self, repo_path, error_message, stash_created=False,
|
|
146
|
+
force=False, failure_phase="git pull"):
|
|
147
|
+
"""Format a pull failure message.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
repo_path: Repository path being updated.
|
|
151
|
+
error_message: Original stderr/stdout-derived git failure text.
|
|
152
|
+
stash_created: Whether local changes were stashed for a force pull.
|
|
153
|
+
force: Whether the failure happened on a force-pull path.
|
|
154
|
+
failure_phase: Phase label such as ``git pull`` or
|
|
155
|
+
``branch checkout``.
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
A user-facing error string with optional recovery guidance.
|
|
159
|
+
"""
|
|
160
|
+
prefix = "Force pull failed" if force else "Pull failed"
|
|
161
|
+
if failure_phase:
|
|
162
|
+
prefix = f"{prefix} during {failure_phase}"
|
|
163
|
+
|
|
164
|
+
resolution = ""
|
|
165
|
+
lowered_error = error_message.lower()
|
|
166
|
+
# Git reports ff-only failures via stderr text instead of a distinct
|
|
167
|
+
# exit code, so use this case-insensitive substring from the standard
|
|
168
|
+
# "Not possible to fast-forward, aborting." message to add guidance.
|
|
169
|
+
if self.GIT_FAST_FORWARD_FAILURE_PHRASE in lowered_error:
|
|
170
|
+
resolution = (
|
|
171
|
+
" Check whether the local and remote branches have diverged "
|
|
172
|
+
"and reconcile them manually before retrying."
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
if stash_created:
|
|
176
|
+
return (
|
|
177
|
+
f"{prefix} for {repo_path}. "
|
|
178
|
+
f"{self._format_stash_restore_guidance(repo_path)} "
|
|
179
|
+
f"Details: {error_message}{resolution}"
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
return f"{prefix} for {repo_path}: {error_message}{resolution}"
|
|
183
|
+
|
|
184
|
+
@staticmethod
|
|
185
|
+
def _subprocess_error_message(error):
|
|
186
|
+
"""Return the most helpful message from a subprocess-related error."""
|
|
187
|
+
return (error.stderr or error.stdout or str(error)).strip()
|
|
188
|
+
|
|
47
189
|
def add(self, run_args):
|
|
48
190
|
"""
|
|
49
191
|
####################################################################################################################
|
|
@@ -322,8 +464,21 @@ class RepoAction(Action):
|
|
|
322
464
|
return {"return": 0, "value": os.path.basename(
|
|
323
465
|
url).replace(".git", "")}
|
|
324
466
|
|
|
467
|
+
def _is_shallow_repo(self, repo_path):
|
|
468
|
+
"""Return True if the git repository at *repo_path* is a shallow clone."""
|
|
469
|
+
try:
|
|
470
|
+
result = subprocess.run(
|
|
471
|
+
['git', '-C', repo_path, 'rev-parse', '--is-shallow-repository'],
|
|
472
|
+
capture_output=True,
|
|
473
|
+
text=True,
|
|
474
|
+
)
|
|
475
|
+
return result.returncode == 0 and result.stdout.strip() == 'true'
|
|
476
|
+
except (subprocess.SubprocessError, FileNotFoundError, OSError):
|
|
477
|
+
return False
|
|
478
|
+
|
|
325
479
|
def pull_repo(self, repo_url, branch=None, checkout=None, tag=None,
|
|
326
|
-
pat=None, ssh=None, ignore_on_conflict=False, repo_path=None, force=False
|
|
480
|
+
pat=None, ssh=None, ignore_on_conflict=False, repo_path=None, force=False,
|
|
481
|
+
shallow=False, depth=None, extra_git_args=None, fast_forward_only=False):
|
|
327
482
|
|
|
328
483
|
# Determine the checkout path from environment or default
|
|
329
484
|
repo_base_path = self.repos_path # either the value will be from 'MLC_REPOS'
|
|
@@ -358,20 +513,41 @@ class RepoAction(Action):
|
|
|
358
513
|
repo_path = os.path.join(repo_base_path, repo_download_name)
|
|
359
514
|
|
|
360
515
|
try:
|
|
516
|
+
# Compute depth argument: --shallow implies depth=1; explicit
|
|
517
|
+
# --depth=N takes precedence
|
|
518
|
+
clone_depth = None
|
|
519
|
+
if depth is not None:
|
|
520
|
+
try:
|
|
521
|
+
clone_depth = int(depth)
|
|
522
|
+
except (TypeError, ValueError):
|
|
523
|
+
return {
|
|
524
|
+
"return": 1, "error": f"Invalid value for --depth: {depth!r}. Must be a positive integer."}
|
|
525
|
+
if clone_depth < 1:
|
|
526
|
+
return {
|
|
527
|
+
"return": 1, "error": f"Invalid value for --depth: {clone_depth}. Must be a positive integer."}
|
|
528
|
+
elif shallow:
|
|
529
|
+
clone_depth = 1
|
|
530
|
+
|
|
531
|
+
# Parse extra_git_args into a list
|
|
532
|
+
extra_args = []
|
|
533
|
+
if extra_git_args:
|
|
534
|
+
if isinstance(extra_git_args, list):
|
|
535
|
+
extra_args = extra_git_args
|
|
536
|
+
else:
|
|
537
|
+
extra_args = shlex.split(str(extra_git_args))
|
|
538
|
+
|
|
361
539
|
# If the directory doesn't exist, clone it
|
|
362
540
|
if not os.path.exists(repo_path):
|
|
363
541
|
logger.info(f"Cloning repository {repo_url} to {repo_path}...")
|
|
364
542
|
|
|
365
|
-
# Build clone command
|
|
366
|
-
clone_command = ['git', 'clone'
|
|
543
|
+
# Build clone command
|
|
544
|
+
clone_command = ['git', 'clone']
|
|
367
545
|
if branch:
|
|
368
|
-
clone_command
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
repo_url,
|
|
374
|
-
repo_path]
|
|
546
|
+
clone_command += ['--branch', branch]
|
|
547
|
+
if clone_depth is not None:
|
|
548
|
+
clone_command += ['--depth', str(clone_depth)]
|
|
549
|
+
clone_command += extra_args
|
|
550
|
+
clone_command += [repo_url, repo_path]
|
|
375
551
|
|
|
376
552
|
subprocess.run(clone_command, check=True)
|
|
377
553
|
|
|
@@ -433,21 +609,33 @@ class RepoAction(Action):
|
|
|
433
609
|
logger.info(
|
|
434
610
|
"Pulling latest changes...")
|
|
435
611
|
try:
|
|
612
|
+
if fast_forward_only and branch:
|
|
613
|
+
self._checkout_pull_branch(repo_path, branch)
|
|
436
614
|
subprocess.run(
|
|
437
|
-
|
|
615
|
+
self._build_pull_command(
|
|
616
|
+
repo_path, branch, clone_depth, fast_forward_only),
|
|
438
617
|
capture_output=True,
|
|
439
618
|
text=True,
|
|
440
619
|
check=True)
|
|
620
|
+
except RuntimeError as e:
|
|
621
|
+
return {
|
|
622
|
+
"return": 1,
|
|
623
|
+
"error": self._format_pull_error(
|
|
624
|
+
repo_path,
|
|
625
|
+
str(e),
|
|
626
|
+
stash_created=stash_created,
|
|
627
|
+
force=True,
|
|
628
|
+
failure_phase="branch checkout")
|
|
629
|
+
}
|
|
441
630
|
except subprocess.CalledProcessError as e:
|
|
442
|
-
pull_error =
|
|
443
|
-
if stash_created:
|
|
444
|
-
return {
|
|
445
|
-
"return": 1,
|
|
446
|
-
"error": f"Force pull failed during git pull for {repo_path}. Local changes remain in stash. Please run `git -C {repo_path} stash apply` after resolving pull issues. Details: {pull_error}"
|
|
447
|
-
}
|
|
631
|
+
pull_error = self._subprocess_error_message(e)
|
|
448
632
|
return {
|
|
449
633
|
"return": 1,
|
|
450
|
-
"error":
|
|
634
|
+
"error": self._format_pull_error(
|
|
635
|
+
repo_path,
|
|
636
|
+
pull_error,
|
|
637
|
+
stash_created=stash_created,
|
|
638
|
+
force=True)
|
|
451
639
|
}
|
|
452
640
|
logger.info("Repository successfully pulled.")
|
|
453
641
|
|
|
@@ -492,8 +680,28 @@ class RepoAction(Action):
|
|
|
492
680
|
else:
|
|
493
681
|
logger.info(
|
|
494
682
|
"No local changes detected. Pulling latest changes...")
|
|
495
|
-
|
|
496
|
-
|
|
683
|
+
try:
|
|
684
|
+
if fast_forward_only and branch:
|
|
685
|
+
self._checkout_pull_branch(repo_path, branch)
|
|
686
|
+
subprocess.run(
|
|
687
|
+
self._build_pull_command(
|
|
688
|
+
repo_path, branch, clone_depth, fast_forward_only),
|
|
689
|
+
capture_output=True,
|
|
690
|
+
text=True,
|
|
691
|
+
check=True)
|
|
692
|
+
except RuntimeError as e:
|
|
693
|
+
return {
|
|
694
|
+
"return": 1,
|
|
695
|
+
"error": self._format_pull_error(
|
|
696
|
+
repo_path, str(e), failure_phase="branch checkout")
|
|
697
|
+
}
|
|
698
|
+
except subprocess.CalledProcessError as e:
|
|
699
|
+
pull_error = self._subprocess_error_message(e)
|
|
700
|
+
return {
|
|
701
|
+
"return": 1,
|
|
702
|
+
"error": self._format_pull_error(
|
|
703
|
+
repo_path, pull_error)
|
|
704
|
+
}
|
|
497
705
|
logger.info("Repository successfully pulled.")
|
|
498
706
|
|
|
499
707
|
if tag:
|
|
@@ -533,6 +741,8 @@ class RepoAction(Action):
|
|
|
533
741
|
|
|
534
742
|
return {"return": 0}
|
|
535
743
|
|
|
744
|
+
except RuntimeError as e:
|
|
745
|
+
return {'return': 1, 'error': str(e)}
|
|
536
746
|
except subprocess.CalledProcessError as e:
|
|
537
747
|
return {'return': 1, 'error': f"Git command failed: {e}"}
|
|
538
748
|
except Exception as e:
|
|
@@ -558,10 +768,13 @@ class RepoAction(Action):
|
|
|
558
768
|
|
|
559
769
|
|
|
560
770
|
- `--checkout <commit_sha>`: Checks out a specific commit after cloning (applicable when the repository exists locally).
|
|
561
|
-
- `--branch <branch_name>`: Checks out a specific branch
|
|
771
|
+
- `--branch <branch_name>`: Checks out a specific branch while cloning a new repository. When strict fast-forward-only pulls are requested for an existing checkout, it also selects the local branch to switch to before pulling `origin/<branch>`.
|
|
562
772
|
- `--tag <release_tag>`: Checks out a particular release tag.
|
|
563
773
|
- `--pat <access_token>` or `--ssh`: Clones a private repository using a personal access token or SSH.
|
|
564
774
|
- `--force`: For existing repositories with local tracked changes, stashes changes before pull and reapplies them after pull.
|
|
775
|
+
- `--shallow`: Perform a shallow clone with `--depth=1` (fastest for a fresh copy without history). For existing repos, only applied if the repo is already shallow; otherwise ignored with a warning.
|
|
776
|
+
- `--depth=N`: Perform a shallow clone/pull with the specified history depth (e.g. `--depth=5`). For existing repos, `--depth` is only applied when the repository is already a shallow clone; passing `--depth` to a full-history clone would corrupt it and is therefore ignored with a warning.
|
|
777
|
+
- `--extra_git_args=<args>`: Pass additional arguments to the `git clone` command (e.g. `--extra_git_args="--filter=blob:none"`). Only applies when cloning a new repository; not used for pull on existing repos. Accepts only trusted input — arguments are passed directly to git without further validation.
|
|
565
778
|
|
|
566
779
|
Example Output:
|
|
567
780
|
|
|
@@ -592,7 +805,12 @@ class RepoAction(Action):
|
|
|
592
805
|
repo_object.path, os.W_OK):
|
|
593
806
|
repo_folder_name = os.path.basename(repo_object.path)
|
|
594
807
|
res = self.pull_repo(
|
|
595
|
-
repo_folder_name, repo_path=repo_object.path, force=run_args.get(
|
|
808
|
+
repo_folder_name, repo_path=repo_object.path, force=run_args.get(
|
|
809
|
+
'force'),
|
|
810
|
+
shallow=run_args.get('shallow', False),
|
|
811
|
+
depth=run_args.get('depth'),
|
|
812
|
+
extra_git_args=run_args.get('extra_git_args'),
|
|
813
|
+
fast_forward_only=run_args.get('fast_forward_only', False))
|
|
596
814
|
if res['return'] > 0:
|
|
597
815
|
return res
|
|
598
816
|
else:
|
|
@@ -604,6 +822,10 @@ class RepoAction(Action):
|
|
|
604
822
|
ssh = run_args.get('ssh')
|
|
605
823
|
force = run_args.get('force')
|
|
606
824
|
ignore_on_conflict = run_args.get('ignore_on_conflict')
|
|
825
|
+
shallow = run_args.get('shallow', False)
|
|
826
|
+
depth = run_args.get('depth')
|
|
827
|
+
extra_git_args = run_args.get('extra_git_args')
|
|
828
|
+
fast_forward_only = run_args.get('fast_forward_only', False)
|
|
607
829
|
|
|
608
830
|
if sum(bool(var) for var in [branch, checkout, tag]) > 1:
|
|
609
831
|
return {
|
|
@@ -617,7 +839,11 @@ class RepoAction(Action):
|
|
|
617
839
|
pat,
|
|
618
840
|
ssh,
|
|
619
841
|
ignore_on_conflict=ignore_on_conflict,
|
|
620
|
-
force=force
|
|
842
|
+
force=force,
|
|
843
|
+
shallow=shallow,
|
|
844
|
+
depth=depth,
|
|
845
|
+
extra_git_args=extra_git_args,
|
|
846
|
+
fast_forward_only=fast_forward_only)
|
|
621
847
|
if res['return'] > 0:
|
|
622
848
|
return res
|
|
623
849
|
|
|
@@ -0,0 +1,83 @@
|
|
|
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
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import tempfile
|
|
2
|
+
import unittest
|
|
3
|
+
from unittest.mock import patch
|
|
4
|
+
|
|
5
|
+
from mlc.script_action import ScriptAction
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class _Parent:
|
|
9
|
+
def __init__(self, repos_path=None):
|
|
10
|
+
self.repos_path = repos_path
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ScriptActionApptainerTest(unittest.TestCase):
|
|
14
|
+
def test_apptainer_delegates_to_script_module(self):
|
|
15
|
+
action = ScriptAction(_Parent(tempfile.gettempdir()))
|
|
16
|
+
run_args = {"tags": "detect,os"}
|
|
17
|
+
expected = {"return": 0}
|
|
18
|
+
|
|
19
|
+
with patch.object(
|
|
20
|
+
ScriptAction,
|
|
21
|
+
"call_script_module_function",
|
|
22
|
+
return_value=expected) as call_script_module_function:
|
|
23
|
+
result = action.apptainer(run_args)
|
|
24
|
+
|
|
25
|
+
self.assertEqual(result, expected)
|
|
26
|
+
call_script_module_function.assert_called_once_with(
|
|
27
|
+
"apptainer", run_args)
|
|
28
|
+
|
|
29
|
+
def test_auto_pull_uses_fast_forward_only_for_mlperf_automations(self):
|
|
30
|
+
with tempfile.TemporaryDirectory() as repos_path:
|
|
31
|
+
action = ScriptAction(_Parent(repos_path))
|
|
32
|
+
|
|
33
|
+
with patch.object(
|
|
34
|
+
ScriptAction,
|
|
35
|
+
"find_target_folder",
|
|
36
|
+
return_value=None), \
|
|
37
|
+
patch.object(
|
|
38
|
+
ScriptAction,
|
|
39
|
+
"access",
|
|
40
|
+
return_value={"return": 1, "error": "pull failed"}) as access:
|
|
41
|
+
result = action.call_script_module_function("run", {})
|
|
42
|
+
|
|
43
|
+
self.assertEqual(result["return"], 1)
|
|
44
|
+
access.assert_called_once_with({
|
|
45
|
+
"automation": "repo",
|
|
46
|
+
"action": "pull",
|
|
47
|
+
"repo": "mlcommons@mlperf-automations",
|
|
48
|
+
"branch": "dev",
|
|
49
|
+
"fast_forward_only": True
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
if __name__ == "__main__":
|
|
54
|
+
unittest.main()
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import unittest
|
|
2
|
-
from unittest.mock import patch
|
|
3
|
-
|
|
4
|
-
from mlc.script_action import ScriptAction
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
class _Parent:
|
|
8
|
-
pass
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class ScriptActionApptainerTest(unittest.TestCase):
|
|
12
|
-
def test_apptainer_delegates_to_script_module(self):
|
|
13
|
-
action = ScriptAction(_Parent())
|
|
14
|
-
run_args = {"tags": "detect,os"}
|
|
15
|
-
expected = {"return": 0}
|
|
16
|
-
|
|
17
|
-
with patch.object(
|
|
18
|
-
ScriptAction,
|
|
19
|
-
"call_script_module_function",
|
|
20
|
-
return_value=expected) as call_script_module_function:
|
|
21
|
-
result = action.apptainer(run_args)
|
|
22
|
-
|
|
23
|
-
self.assertEqual(result, expected)
|
|
24
|
-
call_script_module_function.assert_called_once_with(
|
|
25
|
-
"apptainer", run_args)
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
if __name__ == "__main__":
|
|
29
|
-
unittest.main()
|
|
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
|