Coverage for src/features/mise.py: 100.00%

29 statements  

« prev     ^ index     » next       coverage.py v7.17.2, created at 2026-11-08 13:49 +0000

1#!/usr/bin/env python3 

2 

3# Standard libraries 

4from json import loads 

5from os import environ 

6from pathlib import Path 

7from re import MULTILINE, search, sub 

8from typing import Any, Dict, List, Optional, Tuple 

9 

10# Components 

11from ..package.version import Version 

12from ..system.commands import Commands 

13 

14# Mise class 

15class Mise: 

16 

17 # Constants 

18 BACKEND_DEFAULT: str = 'pipx' 

19 BACKENDS_PYTHON: List[str] = ['pipx', 'pypi'] 

20 BINARY: str = 'mise' 

21 CONFIG_FILE: str = 'config.toml' 

22 CONFIG_LOCAL: str = 'mise.toml' 

23 ENV_CONFIG_DIR: str = 'MISE_CONFIG_DIR' 

24 ENV_DATA_DIR: str = 'MISE_DATA_DIR' 

25 ENV_GLOBAL_CONFIG_FILE: str = 'MISE_GLOBAL_CONFIG_FILE' 

26 ENV_XDG_CONFIG_HOME: str = 'XDG_CONFIG_HOME' 

27 INSTALLS: str = 'installs' 

28 VERSION_LATEST: str = 'latest' 

29 

30 # Arguments 

31 @staticmethod 

32 def arguments(config: str, specification: str = '') -> List[str]: # pragma: no cover 

33 

34 # Use default configuration 

35 if config and specification \ 

36 and Path(config).expanduser().resolve() == Mise.target(specification): 

37 return [] 

38 

39 # Use global configuration 

40 if config and Path(config).expanduser().resolve() == Mise.config_global(): 

41 return [ 

42 '--global', 

43 ] 

44 

45 # Use configuration file 

46 if config: 

47 return [ 

48 '--path', 

49 config, 

50 ] 

51 

52 # Use local configuration 

53 return [] 

54 

55 # Dependencies 

56 @staticmethod 

57 def dependencies( 

58 binary: str, 

59 package: str, 

60 version: str = VERSION_LATEST, 

61 ) -> Optional[bool]: # pragma: no cover 

62 

63 # Variables 

64 config: str 

65 provided: str 

66 tool: Optional[Tuple[str, str]] 

67 

68 # Ignore missing mise 

69 if not Mise.exists(): 

70 return None 

71 

72 # Detect packages 

73 provided = Commands.output(Mise.BINARY, [ 

74 'which', 

75 binary, 

76 ], quiet=True) 

77 tool = Mise.tool(provided) if provided else None # pragma: no cover 

78 

79 # Upgrade packages 

80 if provided and version == Mise.VERSION_LATEST: # pragma: no cover 

81 return Commands.mise([ 

82 'upgrade', 

83 tool[0] if tool else package, 

84 ]) 

85 

86 # Detect configuration file 

87 if Mise.managed(): # pragma: no cover 

88 config = Mise.config() 

89 elif provided: # pragma: no cover 

90 config = tool[1] if tool else '' 

91 else: # pragma: no cover 

92 return None 

93 

94 # Install packages in the configuration file 

95 specification: str = f'{package}@{version}' # pragma: no cover 

96 return Commands.mise( # pragma: no cover 

97 ['use'] + Mise.arguments(config, specification) + [specification]) 

98 

99 # Configuration 

100 @staticmethod 

101 def config() -> str: # pragma: no cover 

102 

103 # Detect configuration file 

104 tool: Optional[Tuple[str, str]] = Mise.tool(Version.path()) 

105 if tool: # pragma: no cover 

106 return tool[1] 

107 

108 # Fallback 

109 return '' 

110 

111 # Configuration global 

112 @staticmethod 

113 def config_global() -> Path: # pragma: no cover 

114 

115 # Detect custom global configuration file 

116 config_file: str = environ.get(Mise.ENV_GLOBAL_CONFIG_FILE, '') 

117 if config_file: 

118 return Path(config_file).expanduser().resolve() 

119 

120 # Detect global configuration folder 

121 config_dir: str = environ.get(Mise.ENV_CONFIG_DIR, '') 

122 if not config_dir: 

123 config_dir = str( 

124 Path(environ.get(Mise.ENV_XDG_CONFIG_HOME, '~/.config')) / Mise.BINARY) 

125 

126 # Result 

127 return (Path(config_dir) / Mise.CONFIG_FILE).expanduser().resolve() 

128 

129 # Exists 

130 @staticmethod 

131 def exists() -> bool: # pragma: no cover 

132 

133 # Check if binary exists 

134 return Commands.exists(Mise.BINARY) 

135 

136 # Installation 

137 @staticmethod 

138 def installation() -> str: 

139 

140 # Variables 

141 parts = Path(Version.path()).parts 

142 

143 # Detect custom mise installs 

144 data_dir: str = environ.get(Mise.ENV_DATA_DIR, '') 

145 if data_dir: # pragma: no cover 

146 installs = Path(data_dir).expanduser().parts + (Mise.INSTALLS, ) 

147 if parts[0:len(installs)] == installs and len(parts) > len(installs): 

148 return parts[len(installs)] # pragma: no cover 

149 

150 # Detect default mise installs 

151 for index in range(len(parts) - 2): 

152 if parts[index] == Mise.BINARY and parts[index + 1] == Mise.INSTALLS: 

153 return parts[index + 2] # pragma: no cover 

154 

155 # Fallback 

156 return '' 

157 

158 # Managed 

159 @staticmethod 

160 def managed() -> bool: 

161 

162 # Check if running from mise 

163 return bool(Mise.installation()) 

164 

165 # Target 

166 @staticmethod 

167 def target(specification: str) -> Optional[Path]: # pragma: no cover 

168 

169 # Detect default configuration file 

170 output: str = Commands.output(Mise.BINARY, [ 

171 'use', 

172 '--dry-run', 

173 specification, 

174 ], errors=True) 

175 match = search( 

176 r'^mise would \w+ (.+) \(', 

177 sub(r'\x1b\[[0-9;]*m', '', output), 

178 MULTILINE, 

179 ) 

180 if match: # pragma: no cover 

181 return Path(match.group(1)).expanduser().resolve() 

182 

183 # Fallback 

184 return None 

185 

186 # Tool 

187 @staticmethod 

188 def tool(path: str) -> Optional[Tuple[str, str]]: # pragma: no cover 

189 

190 # Variables 

191 resolved: Path = Path(path).resolve() 

192 

193 # Find mise tool 

194 for name, entries in Mise.tools().items(): # pragma: no cover 

195 for entry in entries: 

196 install: str = entry.get('install_path', '') 

197 if not install: 

198 continue 

199 install_path: Path = Path(install).resolve() 

200 if resolved == install_path or install_path in resolved.parents: 

201 return name, str(entry.get('source', {}).get('path', '')) 

202 

203 # Fallback 

204 return None 

205 

206 # Tools 

207 @staticmethod 

208 def tools() -> Dict[str, List[Dict[str, Any]]]: # pragma: no cover 

209 

210 # Ignore missing mise 

211 if not Mise.exists(): 

212 return {} 

213 

214 # Get current mise tools 

215 try: 

216 tools = loads( 

217 Commands.output(Mise.BINARY, [ 

218 'ls', 

219 '--current', 

220 '--json', 

221 ], quiet=True)) 

222 if isinstance(tools, dict): # pragma: no cover 

223 return tools 

224 except ValueError: # pragma: no cover 

225 pass 

226 

227 # Fallback 

228 return {} # pragma: no cover 

229 

230 # Package 

231 @staticmethod 

232 def package(name: str) -> str: # pragma: no cover 

233 

234 # Detect mise tool 

235 tool: Optional[Tuple[str, str]] = Mise.tool(Version.path()) 

236 if tool: # pragma: no cover 

237 return tool[0] 

238 

239 # Detect mise backend 

240 backend: str = Mise.installation().split('-', maxsplit=1)[0] 

241 if backend not in Mise.BACKENDS_PYTHON: 

242 backend = Mise.BACKEND_DEFAULT 

243 

244 # Result 

245 return f'{backend}:{name}'