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
« prev ^ index » next coverage.py v7.17.2, created at 2026-11-08 13:49 +0000
1#!/usr/bin/env python3
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
10# Components
11from ..package.version import Version
12from ..system.commands import Commands
14# Mise class
15class Mise:
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'
30 # Arguments
31 @staticmethod
32 def arguments(config: str, specification: str = '') -> List[str]: # pragma: no cover
34 # Use default configuration
35 if config and specification \
36 and Path(config).expanduser().resolve() == Mise.target(specification):
37 return []
39 # Use global configuration
40 if config and Path(config).expanduser().resolve() == Mise.config_global():
41 return [
42 '--global',
43 ]
45 # Use configuration file
46 if config:
47 return [
48 '--path',
49 config,
50 ]
52 # Use local configuration
53 return []
55 # Dependencies
56 @staticmethod
57 def dependencies(
58 binary: str,
59 package: str,
60 version: str = VERSION_LATEST,
61 ) -> Optional[bool]: # pragma: no cover
63 # Variables
64 config: str
65 provided: str
66 tool: Optional[Tuple[str, str]]
68 # Ignore missing mise
69 if not Mise.exists():
70 return None
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
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 ])
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
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])
99 # Configuration
100 @staticmethod
101 def config() -> str: # pragma: no cover
103 # Detect configuration file
104 tool: Optional[Tuple[str, str]] = Mise.tool(Version.path())
105 if tool: # pragma: no cover
106 return tool[1]
108 # Fallback
109 return ''
111 # Configuration global
112 @staticmethod
113 def config_global() -> Path: # pragma: no cover
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()
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)
126 # Result
127 return (Path(config_dir) / Mise.CONFIG_FILE).expanduser().resolve()
129 # Exists
130 @staticmethod
131 def exists() -> bool: # pragma: no cover
133 # Check if binary exists
134 return Commands.exists(Mise.BINARY)
136 # Installation
137 @staticmethod
138 def installation() -> str:
140 # Variables
141 parts = Path(Version.path()).parts
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
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
155 # Fallback
156 return ''
158 # Managed
159 @staticmethod
160 def managed() -> bool:
162 # Check if running from mise
163 return bool(Mise.installation())
165 # Target
166 @staticmethod
167 def target(specification: str) -> Optional[Path]: # pragma: no cover
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()
183 # Fallback
184 return None
186 # Tool
187 @staticmethod
188 def tool(path: str) -> Optional[Tuple[str, str]]: # pragma: no cover
190 # Variables
191 resolved: Path = Path(path).resolve()
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', ''))
203 # Fallback
204 return None
206 # Tools
207 @staticmethod
208 def tools() -> Dict[str, List[Dict[str, Any]]]: # pragma: no cover
210 # Ignore missing mise
211 if not Mise.exists():
212 return {}
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
227 # Fallback
228 return {} # pragma: no cover
230 # Package
231 @staticmethod
232 def package(name: str) -> str: # pragma: no cover
234 # Detect mise tool
235 tool: Optional[Tuple[str, str]] = Mise.tool(Version.path())
236 if tool: # pragma: no cover
237 return tool[0]
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
244 # Result
245 return f'{backend}:{name}'