@@ -262,16 +262,22 @@ def probe_frameworks() -> ProbeResult:
262262
263263
264264def probe_pip_packages () -> ProbeResult :
265- """Get installed pip packages, sorted alphabetically (case-insensitive)."""
266- out = _run_cmd ("python3 -m pip freeze 2>/dev/null" ) or _run_cmd ("pip freeze" )
267- if out is None :
268- return ProbeResult .failure ("pip freeze failed" )
269-
270- packages = sorted (
271- [line .strip () for line in out .splitlines () if line .strip ()],
272- key = lambda s : s .lower (),
273- )
274- return ProbeResult .success (packages )
265+ """Get installed pip packages from multiple sources, labeled by source."""
266+ result : dict [str , list [str ]] = {}
267+ for label , cmd in [
268+ ("python3" , "python3 -m pip freeze 2>/dev/null" ),
269+ ("pip" , "pip freeze 2>/dev/null" ),
270+ ("uv" , "uv pip freeze 2>/dev/null" ),
271+ ]:
272+ out = _run_cmd (cmd )
273+ if out :
274+ pkgs = sorted (
275+ [line .strip () for line in out .splitlines () if line .strip () and not line .startswith ("#" )],
276+ key = lambda s : s .lower (),
277+ )
278+ if pkgs :
279+ result [label ] = pkgs
280+ return ProbeResult .success (result ) if result else ProbeResult .failure ("all pip freeze variants failed" )
275281
276282
277283# ============================================================================
@@ -363,13 +369,23 @@ def load_fingerprint(path: Path) -> dict[str, Any] | None:
363369]
364370
365371
366- def _parse_pip_packages (packages : list [str ]) -> dict [str , str ]:
372+ def _parse_pip_packages (packages : list [str ] | dict [ str , list [ str ]] ) -> dict [str , str ]:
367373 """Parse pip freeze output into {package_name: version} dict.
368374
375+ Accepts either a flat list (legacy) or a labeled dict of lists (new format).
376+ When given a dict, merges all sources (later sources overwrite earlier).
377+
369378 Handles both == and @ formats:
370379 torch==2.6.0 -> {"torch": "2.6.0"}
371380 foo @ file:///... -> {"foo": "file:///..."}
372381 """
382+ # Normalize: flatten labeled dict into a single list
383+ if isinstance (packages , dict ):
384+ flat : list [str ] = []
385+ for pkg_list in packages .values ():
386+ flat .extend (pkg_list )
387+ packages = flat
388+
373389 result = {}
374390 for line in packages :
375391 if "==" in line :
@@ -603,20 +619,19 @@ def find_python():
603619PY = find_python()
604620
605621def pip_pkgs():
606- pkgs = set()
607- for cmd in [
608- f'{{PY}} -m pip freeze 2>/dev/null'.format(PY=PY),
609- 'python3 -m pip freeze 2>/dev/null',
610- 'pip freeze 2>/dev/null',
611- 'uv pip freeze 2>/dev/null',
622+ result = {{}}
623+ for label, cmd in [
624+ (PY, f'{{PY}} -m pip freeze 2>/dev/null'.format(PY=PY) ),
625+ ( 'python3', 'python3 -m pip freeze 2>/dev/null') ,
626+ ( 'pip', 'pip freeze 2>/dev/null') ,
627+ ( 'uv', 'uv pip freeze 2>/dev/null') ,
612628 ]:
613629 out = run(cmd)
614630 if out:
615- for line in out.splitlines():
616- line = line.strip()
617- if line and not line.startswith('#'):
618- pkgs.add(line)
619- return sorted(pkgs, key=lambda s: s.lower())
631+ pkgs = sorted([l.strip() for l in out.splitlines() if l.strip() and not l.startswith('#')], key=lambda s: s.lower())
632+ if pkgs:
633+ result[label] = pkgs
634+ return result
620635
621636def gpu_info():
622637 out = run('nvidia-smi --query-gpu=name,driver_version,memory.total --format=csv,noheader')
0 commit comments