Coverage for lintro / tools / core / install_context.py: 71%
48 statements
« prev ^ index » next coverage.py v7.13.0, created at 2026-04-03 18:53 +0000
« prev ^ index » next coverage.py v7.13.0, created at 2026-04-03 18:53 +0000
1"""Runtime context detection for install-aware commands.
3Detects how lintro was installed (Homebrew, pip, Docker, development),
4what package managers are available, and whether the process is running
5in CI. Strategy-specific install/upgrade hints are now handled by
6:mod:`lintro.tools.core.install_strategies`.
8Usage:
9 from lintro.tools.core.install_context import RuntimeContext
11 ctx = RuntimeContext.detect()
12 print(ctx.install_context) # "pip"
13 print(ctx.environment.has("uv")) # True
14"""
16from __future__ import annotations
18import os
19import platform
20import sys
21from dataclasses import dataclass
23from lintro.enums.install_context import CISystem, InstallContext
24from lintro.tools.core.install_strategies.environment import InstallEnvironment
27@dataclass(frozen=True)
28class RuntimeContext:
29 """Detected runtime context for install-aware commands.
31 Attributes:
32 install_context: How lintro was installed.
33 platform_label: Platform string (e.g., "macOS arm64", "Linux x86_64").
34 environment: Detected package manager availability.
35 is_ci: Whether running in a CI environment.
36 ci_name: Name of the CI system if detected.
37 """
39 install_context: InstallContext
40 platform_label: str
41 environment: InstallEnvironment
42 is_ci: bool
43 ci_name: CISystem | None = None
45 @classmethod
46 def detect(cls) -> RuntimeContext:
47 """Detect the current runtime context.
49 Returns:
50 RuntimeContext with detected values.
51 """
52 ctx = _detect_install_context()
53 return cls(
54 install_context=ctx,
55 platform_label=_detect_platform_label(),
56 environment=InstallEnvironment.detect(ctx),
57 is_ci=_is_ci(),
58 ci_name=CISystem.detect(),
59 )
62def _detect_install_context() -> InstallContext:
63 """Detect how lintro was installed based on the executable path."""
64 # Docker: check for Docker indicators
65 if (
66 os.path.exists("/.dockerenv")
67 or os.environ.get("LINTRO_DOCKER") == "1"
68 or os.environ.get("CONTAINER") == "docker"
69 ):
70 return InstallContext.DOCKER
72 # Resolve symlinks so pip installs under /opt/homebrew/lib aren't
73 # misclassified as Homebrew formula installs.
74 exe_path = os.path.realpath(sys.executable)
75 install_path = os.path.realpath(__file__)
77 # Homebrew: resolved path contains /Cellar/ (formula install)
78 if "/Cellar/lintro-bin/" in install_path or "/Cellar/lintro-bin/" in exe_path:
79 return InstallContext.HOMEBREW_BIN
80 if "/Cellar/lintro/" in install_path or "/Cellar/lintro/" in exe_path:
81 return InstallContext.HOMEBREW_FULL
82 if "/homebrew/" in install_path.lower() and "lintro" in install_path.lower():
83 # Catch Homebrew paths that don't use Cellar (e.g., linuxbrew)
84 if "lintro-bin" in install_path:
85 return InstallContext.HOMEBREW_BIN
86 return InstallContext.HOMEBREW_FULL
88 # Development: running from a git checkout
89 # install_path is lintro/tools/core/install_context.py — 4 levels to repo root
90 source_root = os.path.dirname(
91 os.path.dirname(os.path.dirname(os.path.dirname(install_path))),
92 )
93 # Use os.path.exists (not isdir) to also detect .git files (worktrees/submodules)
94 if os.path.exists(os.path.join(source_root, ".git")):
95 return InstallContext.DEVELOPMENT
97 # Default: pip/uv install
98 return InstallContext.PIP
101def _detect_platform_label() -> str:
102 """Get a human-readable platform label."""
103 system = platform.system()
104 machine = platform.machine()
106 os_names: dict[str, str] = {
107 "Darwin": "macOS",
108 "Linux": "Linux",
109 "Windows": "Windows",
110 }
111 os_label = os_names.get(system, system)
112 return f"{os_label} {machine}"
115def _is_ci() -> bool:
116 """Detect if running in a CI environment.
118 Parses the generic ``CI`` env var as a boolean (``CI=false`` is not CI)
119 and falls back to specific CI system detection via :class:`CISystem`.
120 """
121 ci_value = os.environ.get("CI", "").lower()
122 if ci_value in ("1", "true", "yes", "on"):
123 return True
124 if ci_value in ("0", "false", "no", "off"):
125 return False
126 return CISystem.detect() is not None