@cli.command(context_settings={"help_option_names": ["-h", "--help"]})
@rich_config(help_config=SLEAP_HELP_CONFIG)
@click.option(
"--json",
"output_json",
is_flag=True,
help="Output diagnostics as JSON for programmatic use.",
)
@click.option(
"-o",
"--output",
"output_file",
default=None,
help="Save output to file. Use '-o auto' for auto-timestamped filename.",
)
@click.option(
"--commit",
"show_commit",
is_flag=True,
default=False,
help=(
"Resolve and display the SLEAP commit hash for release installs by "
"looking up the release tag on GitHub (off by default; needs network)."
),
)
def doctor(output_json: bool, output_file: Optional[str], show_commit: bool) -> None:
"""Show system diagnostics for troubleshooting.
Displays detailed information about your system configuration,
including Python environment, GPU status, package versions,
UV/conda configuration, and more.
This output is designed to be copy-pasted when reporting issues.
[dim]Examples:[/]
sleap doctor Show diagnostics
sleap doctor --json Output as JSON
sleap doctor -o Save to auto-timestamped file
sleap doctor -o out.txt Save to specific file
"""
from datetime import datetime
from pathlib import Path
from sleap.system_info import (
PACKAGES,
DIM,
get_detailed_package_info,
get_uv_info_data,
get_conda_info_data,
get_binary_info,
get_nvidia_info,
get_pytorch_info_detailed,
get_memory_info,
get_disk_info,
get_ffmpeg_info,
analyze_path,
short_sha,
resolve_tag_commit,
SLEAP_REPO,
)
if output_json:
_doctor_json(show_commit)
return
console = Console()
all_data = {}
# Print header
console.print()
console.print(f"[bold {SLEAP_TEAL}]SLEAP System Diagnostics[/]")
console.print(f"[{SLEAP_TEAL}]{'=' * 24}[/]")
# Timestamp
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
console.print(f"[{DIM}]Generated:[/] {timestamp}")
console.print()
all_data["timestamp"] = timestamp
# -------------------------------------------------------------------------
# Platform Information
# -------------------------------------------------------------------------
ram_used, ram_avail, ram_total = get_memory_info()
venv_path = os.environ.get("VIRTUAL_ENV", "") or sys.prefix
disk_used, disk_avail, disk_total = get_disk_info(venv_path)
all_data["platform"] = {
"os_name": platform.system(),
"os_release": platform.release(),
"platform_full": platform.platform(),
"machine": platform.machine(),
"processor": platform.processor(),
"ram_used": ram_used,
"ram_total": ram_total,
"disk_used": disk_used,
"disk_total": disk_total,
}
console.print("[Platform]", style=f"bold {SLEAP_BLUE}")
w = _DOCTOR_WIDTHS["platform"] + 1 # +1 for colon
console.print(f" [{DIM}]{'OS:':<{w}}[/] {platform.system()} {platform.release()}")
console.print(f" [{DIM}]{'Platform:':<{w}}[/] {platform.platform()}")
console.print(f" [{DIM}]{'Machine:':<{w}}[/] {platform.machine()}")
processor = platform.processor()
if processor:
console.print(f" [{DIM}]{'Processor:':<{w}}[/] {processor}")
if ram_total:
console.print(f" [{DIM}]{'RAM:':<{w}}[/] {ram_used} / {ram_total}")
if disk_total:
console.print(f" [{DIM}]{'Disk:':<{w}}[/] {disk_used} / {disk_total}")
console.print()
# -------------------------------------------------------------------------
# Python Information
# -------------------------------------------------------------------------
all_data["python"] = {
"version": sys.version.split()[0],
"executable": sys.executable,
"prefix": sys.prefix,
"virtual_env": os.environ.get("VIRTUAL_ENV", ""),
}
console.print("[Python]", style=f"bold {SLEAP_BLUE}")
w = _DOCTOR_WIDTHS["python"] + 1 # +1 for colon
py_ver = sys.version.split()[0]
console.print(f" [{DIM}]{'Version:':<{w}}[/] [{SLEAP_GREEN}]{py_ver}[/]")
console.print(f" [{DIM}]{'Executable:':<{w}}[/] [{SLEAP_CYAN}]{sys.executable}[/]")
console.print(f" [{DIM}]{'Prefix:':<{w}}[/] [{SLEAP_CYAN}]{sys.prefix}[/]")
venv = os.environ.get("VIRTUAL_ENV")
if venv:
console.print(f" [{DIM}]{'Virtual Env:':<{w}}[/] [{SLEAP_CYAN}]{venv}[/]")
console.print()
# -------------------------------------------------------------------------
# UV Information
# -------------------------------------------------------------------------
with console.status(f"[{DIM}]Checking UV...[/]", spinner="dots"):
uv_info = get_uv_info_data()
all_data["uv"] = uv_info
if uv_info:
console.print("[UV]", style=f"bold {SLEAP_BLUE}")
w = _DOCTOR_WIDTHS["uv"] + 1 # +1 for colon
uv_ver = uv_info.version
console.print(f" [{DIM}]{'Version:':<{w}}[/] [{SLEAP_GREEN}]{uv_ver}[/]")
console.print(f" [{DIM}]{'Path:':<{w}}[/] [{SLEAP_CYAN}]{uv_info.path}[/]")
uv_cache = uv_info.cache_dir
console.print(f" [{DIM}]{'Cache Dir:':<{w}}[/] [{SLEAP_CYAN}]{uv_cache}[/]")
uv_tool = uv_info.tool_dir
console.print(f" [{DIM}]{'Tool Dir:':<{w}}[/] [{SLEAP_CYAN}]{uv_tool}[/]")
uv_tool_bin = uv_info.tool_bin_dir
console.print(
f" [{DIM}]{'Tool Bin Dir:':<{w}}[/] [{SLEAP_CYAN}]{uv_tool_bin}[/]"
)
uv_py_dir = uv_info.python_dir
console.print(f" [{DIM}]{'Python Dir:':<{w}}[/] [{SLEAP_CYAN}]{uv_py_dir}[/]")
if uv_info.installed_tools:
tools_str = ", ".join(uv_info.installed_tools)
console.print(f" [{DIM}]{'Installed Tools:':<{w}}[/] {tools_str}")
console.print()
# UV Config
console.print("[UV Config]", style=f"bold {SLEAP_BLUE}")
w = _DOCTOR_WIDTHS["uv_config"] + 1 # +1 for colon
if uv_info.default_python:
default_py = uv_info.default_python
console.print(
f" [{DIM}]{'Default Python:':<{w}}[/] [{SLEAP_CYAN}]{default_py}[/]"
)
else:
console.print(
f" [{DIM}]{'Default Python:':<{w}}[/] [{DIM}](not configured)[/]"
)
if uv_info.resolved_python:
resolved_py = uv_info.resolved_python
console.print(
f" [{DIM}]{'Resolved Python:':<{w}}[/] [{SLEAP_CYAN}]{resolved_py}[/]"
)
pref = uv_info.python_preference or "managed"
is_default = not uv_info.python_preference
pref_display = f"{pref} [{DIM}](default)[/]" if is_default else pref
console.print(f" [{DIM}]{'Python Preference:':<{w}}[/] {pref_display}")
res = uv_info.resolution_strategy or "highest"
is_default = not uv_info.resolution_strategy
res_display = f"{res} [{DIM}](default)[/]" if is_default else res
console.print(f" [{DIM}]{'Resolution:':<{w}}[/] {res_display}")
idx = uv_info.index_strategy or "first-index"
is_default = not uv_info.index_strategy
idx_display = f"{idx} [{DIM}](default)[/]" if is_default else idx
console.print(f" [{DIM}]{'Index Strategy:':<{w}}[/] {idx_display}")
pre = uv_info.prerelease or "if-necessary"
is_default = not uv_info.prerelease
pre_display = f"{pre} [{DIM}](default)[/]" if is_default else pre
console.print(f" [{DIM}]{'Prerelease:':<{w}}[/] {pre_display}")
console.print()
# -------------------------------------------------------------------------
# Conda Information
# -------------------------------------------------------------------------
with console.status(f"[{DIM}]Checking conda...[/]", spinner="dots"):
conda_info = get_conda_info_data()
all_data["conda"] = conda_info
if conda_info:
console.print("[Conda]", style=f"bold {SLEAP_BLUE}")
w = _DOCTOR_WIDTHS["conda"] + 1 # +1 for colon
if conda_info.active:
console.print(f" [{DIM}]{'Status:':<{w}}[/] [{SLEAP_YELLOW}]ACTIVE[/]")
console.print(f" [{DIM}]{'Environment:':<{w}}[/] {conda_info.environment}")
conda_prefix = conda_info.prefix
console.print(
f" [{DIM}]{'Prefix:':<{w}}[/] [{SLEAP_CYAN}]{conda_prefix}[/]"
)
else:
console.print(f" [{DIM}]{'Status:':<{w}}[/] installed but not activated")
if conda_info.version:
console.print(f" [{DIM}]{'Version:':<{w}}[/] {conda_info.version}")
if conda_info.auto_activate_base is not None:
status = "True" if conda_info.auto_activate_base else "False"
color = SLEAP_RED if conda_info.auto_activate_base else SLEAP_GREEN
console.print(
f" [{DIM}]{'auto_activate_base:':<{w}}[/] [{color}]{status}[/]"
)
if conda_info.auto_activate_base:
console.print(
f" [{SLEAP_YELLOW}]WARNING: auto_activate_base=True "
f"may interfere with uv[/]"
)
console.print(
f" [{DIM}]Suggestion: "
f"conda config --set auto_activate_base false[/]"
)
if conda_info.sleap_packages:
pkgs_str = ", ".join(conda_info.sleap_packages)
console.print(
f" [{DIM}]{'SLEAP in conda:':<{w}}[/] [{SLEAP_RED}]{pkgs_str}[/]"
)
console.print(
f" [{SLEAP_YELLOW}]WARNING: Conda SLEAP packages "
f"may conflict with uv/pip[/]"
)
console.print()
# -------------------------------------------------------------------------
# GPU / CUDA Information
# -------------------------------------------------------------------------
with console.status(f"[{DIM}]Checking GPU...[/]", spinner="dots"):
nvidia_driver, system_cuda, gpus = get_nvidia_info()
all_data["nvidia_driver"] = nvidia_driver
all_data["system_cuda"] = system_cuda
all_data["gpus"] = gpus
with console.status(f"[{DIM}]Checking PyTorch...[/]", spinner="dots"):
pytorch_version, pytorch_accelerator, pytorch_cuda = get_pytorch_info_detailed()
all_data["pytorch_version"] = pytorch_version
all_data["pytorch_accelerator"] = pytorch_accelerator
all_data["pytorch_cuda"] = pytorch_cuda
console.print("[GPU / CUDA]", style=f"bold {SLEAP_BLUE}")
w = _DOCTOR_WIDTHS["gpu"] + 1 # +1 for colon
if nvidia_driver:
driver_str = nvidia_driver
if system_cuda:
driver_str += f" (CUDA {system_cuda})"
console.print(
f" [{DIM}]{'NVIDIA Driver:':<{w}}[/] [{SLEAP_GREEN}]{driver_str}[/]"
)
for i, gpu in enumerate(gpus):
console.print(
f" [{DIM}]{f'GPU {i}:':<{w}}[/] [{SLEAP_TEAL}]{gpu.name}[/] "
f"([{SLEAP_GREEN}]{gpu.memory_free}[/] free / {gpu.memory_total})"
)
else:
console.print(f" [{DIM}]{'NVIDIA Driver:':<{w}}[/] Not detected")
if pytorch_version:
pt_str = f"v{pytorch_version}"
if pytorch_accelerator == "cuda":
pt_str += f" ([{SLEAP_GREEN}]CUDA {pytorch_cuda}[/])"
elif pytorch_accelerator == "mps":
pt_str += f" ([{SLEAP_GREEN}]MPS[/])"
else:
pt_str += f" ([{SLEAP_YELLOW}]CPU[/])"
console.print(f" [{DIM}]{'PyTorch:':<{w}}[/] [{SLEAP_TEAL}]{pt_str}[/]")
else:
console.print(f" [{DIM}]{'PyTorch:':<{w}}[/] Not installed")
console.print()
# -------------------------------------------------------------------------
# Package Versions
# -------------------------------------------------------------------------
with console.status(f"[{DIM}]Checking packages...[/]", spinner="dots"):
packages = []
for pkg_name in PACKAGES:
pkg_info = get_detailed_package_info(pkg_name)
if pkg_info:
packages.append(pkg_info)
all_data["packages"] = packages
# Optionally resolve the SLEAP commit from its release tag via GitHub. Only
# done with --commit (it needs network) and only for release installs that
# don't already carry a local commit.
resolved_commits = {}
if show_commit:
sleap_pkg = next((p for p in packages if p.name == "sleap"), None)
if sleap_pkg and not sleap_pkg.git_commit:
with console.status(
f"[{DIM}]Resolving commit from GitHub...[/]", spinner="dots"
):
sha = resolve_tag_commit(SLEAP_REPO, sleap_pkg.version)
if sha:
resolved_commits["sleap"] = sha
all_data["resolved_commits"] = resolved_commits
console.print("[Packages]", style=f"bold {SLEAP_BLUE}")
w = max(len(pkg.name) for pkg in packages) + 1 if packages else 10 # +1 for colon
for pkg in packages:
source_color = (
SLEAP_PURPLE
if pkg.source == "editable"
else SLEAP_ORANGE
if pkg.source == "conda"
else DIM
)
pkg_line = (
f" [{SLEAP_TEAL}]{(pkg.name + ':'):<{w}}[/] "
f"[{SLEAP_GREEN}]v{pkg.version}[/] ([{source_color}]{pkg.source}[/])"
)
if pkg.git_commit:
git_info = f"git:{pkg.git_branch or 'HEAD'}@{short_sha(pkg.git_commit)}"
if pkg.git_dirty:
git_info += "*"
pkg_line += f" [[{SLEAP_PURPLE}]{git_info}[/]]"
elif pkg.name in resolved_commits:
tag_info = f"github:v{pkg.version}@{short_sha(resolved_commits[pkg.name])}"
pkg_line += f" [[{SLEAP_PURPLE}]{tag_info}[/]]"
console.print(pkg_line)
if pkg.editable:
console.print(f" {'':<{w}} Location: [{SLEAP_CYAN}]{pkg.location}[/]")
elif pkg.git_remote:
console.print(f" {'':<{w}} Remote: [{SLEAP_CYAN}]{pkg.git_remote}[/]")
console.print()
# -------------------------------------------------------------------------
# CLI Binaries
# -------------------------------------------------------------------------
with console.status(f"[{DIM}]Checking CLI binaries...[/]", spinner="dots"):
binaries = []
bin_names = ["sleap", "sleap-nn", "sio"]
for bin_name in bin_names:
bin_info = get_binary_info(bin_name)
if bin_info:
binaries.append(bin_info)
# Add ffmpeg binaries
binaries.extend(get_ffmpeg_info())
all_data["binaries"] = binaries
if binaries:
console.print("[CLI Binaries]", style=f"bold {SLEAP_BLUE}")
w = _DOCTOR_WIDTHS["binaries"] + 1 # +1 for colon
for binary in binaries:
source_color = (
SLEAP_TEAL
if binary.source == "venv"
else SLEAP_PURPLE
if binary.source == "uv-tool"
else SLEAP_ORANGE
)
console.print(f" [{SLEAP_TEAL}]{binary.name}[/]:")
bin_path = binary.path
console.print(f" [{DIM}]{'Path:':<{w}}[/] [{SLEAP_CYAN}]{bin_path}[/]")
if binary.real_path != binary.path:
real_path = binary.real_path
console.print(
f" [{DIM}]{'Real Path:':<{w}}[/] [{SLEAP_CYAN}]{real_path}[/]"
)
bin_src = binary.source
console.print(
f" [{DIM}]{'Source:':<{w}}[/] [{source_color}]{bin_src}[/]"
)
if binary.python_path:
py_path = binary.python_path
console.print(
f" [{DIM}]{'Python:':<{w}}[/] [{SLEAP_CYAN}]{py_path}[/]"
)
console.print()
# -------------------------------------------------------------------------
# PATH Analysis
# -------------------------------------------------------------------------
path_entries, path_conflicts = analyze_path()
all_data["path_entries"] = path_entries
all_data["path_conflicts"] = path_conflicts
if path_conflicts:
console.print("[PATH Conflicts]", style=f"bold {SLEAP_RED}")
for conflict in path_conflicts:
console.print(f" [{SLEAP_YELLOW}]WARNING: {conflict}[/]")
console.print()
console.print("[PATH (relevant entries)]", style=f"bold {SLEAP_BLUE}")
relevant_keywords = [
"conda",
"miniconda",
"uv",
".local",
"sleap",
"python",
"venv",
]
for path in path_entries:
if any(kw in path.lower() for kw in relevant_keywords):
console.print(f" [{SLEAP_CYAN}]{path}[/]")
console.print()
# -------------------------------------------------------------------------
# Footer
# -------------------------------------------------------------------------
output_path = None
if output_file:
if output_file == "auto":
file_timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
output_path = Path(f"sleap-doctor-{file_timestamp}.txt")
else:
output_path = Path(output_file)
output_text = _format_doctor_plain(all_data)
output_path.write_text(output_text)
console.print(f"[{DIM}]Copy this output when reporting issues at:[/]")
console.print(f"[{SLEAP_BLUE}]https://github.com/talmolab/sleap/issues[/]")
console.print()
if output_path:
console.print(f"[{SLEAP_GREEN}]Saved to:[/] [{SLEAP_TEAL}]{output_path}[/]")
else:
console.print(
f"[bold {SLEAP_TEAL}]Tip:[/] [{DIM}]Use[/] "
f"[{SLEAP_TEAL}]sleap doctor -o[/] "
f"[{DIM}]to save diagnostics to a file[/]"
)
console.print()