Skip to content

Commit 8df5a9d

Browse files
committed
add update all feature
1 parent 8e76f8b commit 8df5a9d

2 files changed

Lines changed: 258 additions & 16 deletions

File tree

xircuits/library/update_library.py

Lines changed: 203 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from typing import List, Optional, Set
99
from importlib_resources import files, as_file
1010

11+
from xircuits.utils.pathing import resolve_working_dir, components_base_dir
1112
from .core_libs import is_core_library
1213

1314
from xircuits.utils.pathing import (
@@ -51,6 +52,7 @@ def update_library(
5152
dry_run: bool = False,
5253
prune: bool = False,
5354
install_deps: bool = True,
55+
use_latest: bool = False,
5456
) -> str:
5557
"""
5658
Update an installed component library (core or regular).
@@ -67,6 +69,7 @@ def update_library(
6769
dry_run: If True, compute actions and print a unified diff (no files changed).
6870
prune: If True, also archive local-only files/dirs (rename to *.bak).
6971
install_deps: If True (default), update per-library extra and install its requirements.
72+
use_latest: If True, ignore metadata ref and pull latest from default branch.
7073
7174
Returns:
7275
Summary message of update results.
@@ -104,6 +107,7 @@ def update_library(
104107
dry_run=dry_run,
105108
prune=prune,
106109
install_deps=install_deps,
110+
use_latest=use_latest,
107111
)
108112

109113
def _update_from_wheel(
@@ -200,6 +204,7 @@ def _update_from_git(
200204
dry_run: bool,
201205
prune: bool,
202206
install_deps: bool,
207+
use_latest: bool = False,
203208
) -> str:
204209
"""
205210
Update a regular component library from its git repository.
@@ -213,7 +218,7 @@ def _update_from_git(
213218

214219
timestamp = time.strftime("%Y%m%d-%H%M%S")
215220

216-
source_spec = _resolve_source_spec(lib_name, repo, ref)
221+
source_spec = _resolve_source_spec(lib_name, repo, ref, use_latest)
217222
if not source_spec or not source_spec.repo_url:
218223
raise RuntimeError(
219224
f"Could not resolve a repository URL for '{lib_name}'. "
@@ -382,20 +387,29 @@ def _resolve_source_spec(
382387
lib_name: str,
383388
repo_override: Optional[str],
384389
user_ref: Optional[str],
390+
use_latest: bool = False,
385391
) -> Optional[SourceSpec]:
386392
"""
387393
Priority:
388394
0) explicit repo override (CLI/API 'repo=')
389395
1) pyproject.toml [tool.xircuits.components] entry (source + tag/rev)
390396
2) manifest index via get_remote_config()
397+
398+
If use_latest=True, ignore metadata ref and use user_ref (or None for default branch).
391399
"""
392400
# use repo url if specified
393401
if repo_override:
394402
return SourceSpec(repo_url=repo_override, desired_ref=user_ref)
395403

396404
# pyproject metadata
397405
source_url, meta_ref = read_component_metadata_entry(lib_name)
398-
desired_ref = user_ref or meta_ref
406+
407+
# Determine ref: use_latest bypasses metadata ref
408+
if use_latest:
409+
desired_ref = user_ref # None means default branch
410+
else:
411+
desired_ref = user_ref or meta_ref
412+
399413
if source_url:
400414
return SourceSpec(repo_url=source_url, desired_ref=desired_ref)
401415

@@ -674,3 +688,190 @@ def _build_combined_diff(
674688
out.append(diff)
675689

676690
return "\n".join(out)
691+
692+
# ---------- Update All functionality ----------
693+
694+
def update_all_libraries(
695+
dry_run: bool = False,
696+
prune: bool = False,
697+
install_deps: bool = True,
698+
core_only: bool = False,
699+
remote_only: bool = False,
700+
exclude: List[str] = None,
701+
respect_refs: bool = False,
702+
) -> dict:
703+
"""
704+
Update all installed component libraries found in xai_components/.
705+
706+
Args:
707+
dry_run: Preview changes without modifying files
708+
prune: Remove local-only files during update
709+
install_deps: Install/update Python dependencies
710+
core_only: Only update core libraries
711+
remote_only: Only update non-core libraries
712+
exclude: List of library names to skip
713+
respect_refs: Honor pinned refs in metadata (default: pull latest)
714+
715+
Returns:
716+
Dict with 'success', 'failed', 'skipped' lists and summary stats
717+
"""
718+
719+
# Validate conflicting flags
720+
if core_only and remote_only:
721+
raise ValueError("Cannot specify both --core-only and --remote-only")
722+
723+
working_dir = resolve_working_dir()
724+
if working_dir is None:
725+
raise RuntimeError("Xircuits working directory not found. Run 'xircuits init' first.")
726+
727+
exclude_set = set((exclude or []))
728+
exclude_set = {normalize_library_slug(x) for x in exclude_set}
729+
730+
results = {
731+
"success": [],
732+
"failed": [],
733+
"skipped": []
734+
}
735+
736+
# Discover libraries to update
737+
libraries_to_update = _discover_updateable_libraries(
738+
working_dir=working_dir,
739+
core_only=core_only,
740+
remote_only=remote_only,
741+
exclude=exclude_set
742+
)
743+
744+
if not libraries_to_update:
745+
print("No libraries found to update.")
746+
return results
747+
748+
print(f"Found {len(libraries_to_update)} {'library' if len(libraries_to_update) == 1 else 'libraries'} to update")
749+
if dry_run:
750+
print("DRY-RUN MODE: No files will be modified\n")
751+
print()
752+
753+
# Update each library
754+
for lib_name in sorted(libraries_to_update):
755+
try:
756+
print(f"{'='*60}")
757+
print(f"Updating: {lib_name}")
758+
print(f"{'='*60}")
759+
760+
# For --all without --respect-refs, pull latest by setting use_latest=True
761+
message = update_library(
762+
library_name=lib_name,
763+
repo=None,
764+
ref=None,
765+
dry_run=dry_run,
766+
prune=prune,
767+
install_deps=install_deps,
768+
use_latest=not respect_refs,
769+
)
770+
771+
results["success"].append((lib_name, message))
772+
print(f"✓ {lib_name}: {message}\n")
773+
774+
except Exception as e:
775+
error_msg = str(e)
776+
results["failed"].append((lib_name, error_msg))
777+
print(f"✗ {lib_name}: Failed - {error_msg}\n")
778+
# Continue to next library
779+
780+
# Print summary
781+
_print_update_all_summary(results, dry_run)
782+
783+
return results
784+
785+
def _discover_updateable_libraries(
786+
working_dir: Path,
787+
core_only: bool,
788+
remote_only: bool,
789+
exclude: set
790+
) -> List[str]:
791+
"""
792+
Scan xai_components directory and return list of updateable library names.
793+
"""
794+
795+
base_dir = components_base_dir(working_dir)
796+
if not base_dir.exists():
797+
return []
798+
799+
libraries = []
800+
801+
# Check base.py
802+
base_py = base_dir / "base.py"
803+
if base_py.exists() and base_py.is_file():
804+
if not remote_only and "base.py" not in exclude:
805+
if core_only or not remote_only:
806+
libraries.append("base.py")
807+
808+
# Scan xai_* directories
809+
for item in base_dir.glob("xai_*"):
810+
if not item.is_dir():
811+
continue
812+
813+
# Must have __init__.py to be valid
814+
if not (item / "__init__.py").exists():
815+
continue
816+
817+
lib_name = item.name
818+
819+
# Check exclusions
820+
if lib_name in exclude:
821+
continue
822+
823+
# Check core/remote filters
824+
is_core = is_core_library(lib_name)
825+
if core_only and not is_core:
826+
continue
827+
if remote_only and is_core:
828+
continue
829+
830+
libraries.append(lib_name)
831+
832+
return libraries
833+
834+
835+
def _print_update_all_summary(results: dict, dry_run: bool):
836+
"""
837+
Print a formatted summary of update results.
838+
"""
839+
print()
840+
print("="*60)
841+
print("Update All Summary")
842+
print("="*60)
843+
print()
844+
845+
if results["success"]:
846+
print("✓ SUCCEEDED:")
847+
for lib_name, message in results["success"]:
848+
print(f" {lib_name:20} {message}")
849+
print()
850+
851+
if results["failed"]:
852+
print("✗ FAILED:")
853+
for lib_name, error in results["failed"]:
854+
# Truncate long errors
855+
error_display = error if len(error) <= 60 else error[:57] + "..."
856+
print(f" {lib_name:20} {error_display}")
857+
print()
858+
859+
if results["skipped"]:
860+
print("⊘ SKIPPED:")
861+
for lib_name, reason in results["skipped"]:
862+
print(f" {lib_name:20} {reason}")
863+
print()
864+
865+
# Summary counts
866+
total = len(results["success"]) + len(results["failed"]) + len(results["skipped"])
867+
summary_parts = []
868+
if results["success"]:
869+
summary_parts.append(f"{len(results['success'])} succeeded")
870+
if results["failed"]:
871+
summary_parts.append(f"{len(results['failed'])} failed")
872+
if results["skipped"]:
873+
summary_parts.append(f"{len(results['skipped'])} skipped")
874+
875+
mode_suffix = " (dry-run)" if dry_run else ""
876+
print(f"{', '.join(summary_parts)}{mode_suffix}")
877+
print("="*60)

xircuits/start_xircuits.py

Lines changed: 55 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
from .library import list_component_library, install_library, fetch_library, uninstall_library
1111
from .library.index_config import refresh_index
12-
from .library.update_library import update_library
12+
from .library.update_library import update_library, update_all_libraries
1313

1414
from .compiler import compile, recursive_compile
1515
from xircuits.handlers.config import get_config
@@ -146,16 +146,44 @@ def cmd_sync(args, extra_args=[]):
146146
sync_xai_components()
147147

148148
def cmd_update_library(args, extra_args=[]):
149-
150-
message = update_library(
151-
library_name=args.library_name,
152-
repo=args.repo,
153-
ref=args.ref,
154-
dry_run=args.dry_run,
155-
prune=args.prune,
156-
install_deps=args.install_deps,
157-
)
158-
print(message)
149+
if args.all:
150+
151+
# Parse exclude list
152+
exclude_list = []
153+
if args.exclude:
154+
exclude_list = [x.strip() for x in args.exclude.split(',') if x.strip()]
155+
156+
# Validate conflicting flags
157+
if args.core_only and args.remote_only:
158+
print("Error: Cannot specify both --core-only and --remote-only")
159+
return
160+
161+
try:
162+
results = update_all_libraries(
163+
dry_run=args.dry_run,
164+
prune=args.prune,
165+
install_deps=args.install_deps,
166+
core_only=args.core_only,
167+
remote_only=args.remote_only,
168+
exclude=exclude_list,
169+
respect_refs=args.respect_refs,
170+
)
171+
172+
except Exception as e:
173+
print(f"Error: {e}")
174+
return
175+
else:
176+
# single-library update
177+
message = update_library(
178+
library_name=args.library_name,
179+
repo=args.repo,
180+
ref=args.ref,
181+
dry_run=args.dry_run,
182+
prune=args.prune,
183+
install_deps=args.install_deps,
184+
# use_latest defaults to False for single library updates
185+
)
186+
print(message)
159187

160188
def cmd_run(args, extra_args=[]):
161189
original_cwd = args.original_cwd
@@ -276,15 +304,28 @@ def main():
276304
update_parser = subparsers.add_parser(
277305
'update', help='Update a component library with in-place .bak backups.'
278306
)
279-
update_parser.add_argument('library_name', type=str, help='Library to update (e.g., flask)')
307+
update_parser.add_argument('library_name', nargs='?', type=str,
308+
help='Library to update (e.g., flask). Omit with --all.')
280309
update_parser.add_argument('--repo', type=str, default=None, help='Override source repository URL')
281310
update_parser.add_argument('--ref', type=str, default=None, help='Tag/branch/commit to update to')
282311
update_parser.add_argument('--dry-run', action='store_true', help='Preview only; no changes')
283312
update_parser.add_argument('--prune', action='store_true',
284313
help='Prune local-only files/dirs (rename to .bak)')
285314
update_parser.add_argument('--install-deps', nargs='?', const=True, default=True,
286-
type=lambda s: str(s).lower() not in ('0','false','no','off'),
287-
help='Install/update Python deps (default true). Pass false to disable.')
315+
type=lambda s: str(s).lower() not in ('0','false','no','off'),
316+
help='Install/update Python deps (default true). Pass false to disable.')
317+
318+
update_parser.add_argument('--all', action='store_true',
319+
help='Update all installed component libraries')
320+
update_parser.add_argument('--core-only', action='store_true',
321+
help='Update only core libraries (xai_events, xai_template, etc.)')
322+
update_parser.add_argument('--remote-only', action='store_true',
323+
help='Update only remote (non-core) libraries')
324+
update_parser.add_argument('--exclude', type=str, default='',
325+
help='Comma-separated list of libraries to exclude (e.g., gradio,opencv)')
326+
update_parser.add_argument('--respect-refs', action='store_true',
327+
help='Honor pinned refs in metadata (default: pull latest for --all)')
328+
288329
update_parser.set_defaults(func=cmd_update_library)
289330

290331
# 'run' command.

0 commit comments

Comments
 (0)