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

1"""Runtime context detection for install-aware commands. 

2 

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`. 

7 

8Usage: 

9 from lintro.tools.core.install_context import RuntimeContext 

10 

11 ctx = RuntimeContext.detect() 

12 print(ctx.install_context) # "pip" 

13 print(ctx.environment.has("uv")) # True 

14""" 

15 

16from __future__ import annotations 

17 

18import os 

19import platform 

20import sys 

21from dataclasses import dataclass 

22 

23from lintro.enums.install_context import CISystem, InstallContext 

24from lintro.tools.core.install_strategies.environment import InstallEnvironment 

25 

26 

27@dataclass(frozen=True) 

28class RuntimeContext: 

29 """Detected runtime context for install-aware commands. 

30 

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 """ 

38 

39 install_context: InstallContext 

40 platform_label: str 

41 environment: InstallEnvironment 

42 is_ci: bool 

43 ci_name: CISystem | None = None 

44 

45 @classmethod 

46 def detect(cls) -> RuntimeContext: 

47 """Detect the current runtime context. 

48 

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 ) 

60 

61 

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 

71 

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__) 

76 

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 

87 

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 

96 

97 # Default: pip/uv install 

98 return InstallContext.PIP 

99 

100 

101def _detect_platform_label() -> str: 

102 """Get a human-readable platform label.""" 

103 system = platform.system() 

104 machine = platform.machine() 

105 

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}" 

113 

114 

115def _is_ci() -> bool: 

116 """Detect if running in a CI environment. 

117 

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