Coverage for src / taipanstack / core / compat.py: 100%

111 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-03-23 14:54 +0000

1""" 

2Python Version Compatibility and Feature Detection. 

3 

4This module provides runtime detection of Python version and available 

5performance features, enabling version-specific optimizations while 

6maintaining compatibility with Python 3.11+. 

7 

8Following Stack pillars: Security, Stability, Simplicity, Scalability, Compatibility. 

9""" 

10 

11import logging 

12import os 

13import sys 

14from dataclasses import dataclass 

15from enum import StrEnum 

16from typing import Final 

17 

18__all__ = [ 

19 "PY311", 

20 "PY312", 

21 "PY313", 

22 "PY314", 

23 "PY_VERSION", 

24 "PythonFeatures", 

25 "VersionTier", 

26 "get_features", 

27 "get_python_info", 

28 "is_experimental_enabled", 

29] 

30 

31logger = logging.getLogger(__name__) 

32 

33# ============================================================================= 

34# Version Constants 

35# ============================================================================= 

36 

37PY_VERSION: Final = sys.version_info 

38"""Current Python version tuple.""" 

39 

40PY311: Final[bool] = PY_VERSION >= (3, 11) 

41"""True if running Python 3.11 or higher.""" 

42 

43PY312: Final[bool] = PY_VERSION >= (3, 12) 

44"""True if running Python 3.12 or higher.""" 

45 

46PY313: Final[bool] = PY_VERSION >= (3, 13) 

47"""True if running Python 3.13 or higher.""" 

48 

49PY314: Final[bool] = PY_VERSION >= (3, 14) 

50"""True if running Python 3.14 or higher.""" 

51 

52 

53class VersionTier(StrEnum): 

54 """Python version tier for optimization profiles.""" 

55 

56 STABLE = "stable" # 3.11 - fully stable, conservative optimizations 

57 ENHANCED = "enhanced" # 3.12 - improved features, moderate optimizations 

58 MODERN = "modern" # 3.13 - JIT/free-threading available (experimental) 

59 CUTTING_EDGE = "cutting_edge" # 3.14+ - latest optimizations 

60 

61 

62# ============================================================================= 

63# Environment Variables for Experimental Features 

64# ============================================================================= 

65 

66ENV_ENABLE_EXPERIMENTAL = "STACK_ENABLE_EXPERIMENTAL" 

67"""Environment variable to enable experimental features (JIT, free-threading).""" 

68 

69ENV_OPTIMIZATION_LEVEL = "STACK_OPTIMIZATION_LEVEL" 

70"""Optimization level: 0=none, 1=safe, 2=aggressive (requires experimental).""" 

71 

72 

73# ============================================================================= 

74# Feature Detection Functions 

75# ============================================================================= 

76 

77 

78def _check_jit_available() -> bool: 

79 """Check if JIT compiler is available and enabled. 

80 

81 JIT is available in Python 3.13+ when built with --enable-experimental-jit. 

82 """ 

83 if not PY313: # pragma: no cover 

84 return False 

85 

86 try: 

87 # Check if the JIT module exists (Python 3.13+) 

88 # This is a build-time option, not all builds have it 

89 return hasattr(sys, "_jit") or "jit" in sys.flags.__class__.__annotations__ 

90 except (AttributeError, TypeError): 

91 return False 

92 

93 

94def _check_free_threading_available() -> bool: 

95 """Check if free-threading (no-GIL) build is being used. 

96 

97 Free-threading is available in Python 3.13+ experimental builds. 

98 """ 

99 if not PY313: # pragma: no cover 

100 return False 

101 

102 try: 

103 # In free-threaded builds, sys.flags.nogil is True 

104 # or the build was configured with --disable-gil 

105 if hasattr(sys.flags, "nogil"): 

106 return bool(sys.flags.nogil) 

107 

108 # Alternative check for 3.13+ 

109 import sysconfig # noqa: PLC0415 

110 

111 config_args = sysconfig.get_config_var("CONFIG_ARGS") or "" 

112 except (AttributeError, TypeError): 

113 return False 

114 else: 

115 return "--disable-gil" in config_args 

116 

117 

118def _check_mimalloc_available() -> bool: 

119 """Check if mimalloc allocator is being used. 

120 

121 mimalloc is the default allocator in Python 3.13+ on some platforms. 

122 """ 

123 if not PY313: 

124 return False 

125 

126 try: 

127 import sysconfig # noqa: PLC0415 - lazy import for optional module 

128 

129 # Check if built with mimalloc 

130 config_args = sysconfig.get_config_var("CONFIG_ARGS") or "" 

131 return "mimalloc" in config_args.lower() 

132 except (AttributeError, TypeError): 

133 return False 

134 

135 

136def _check_tail_call_interpreter() -> bool: 

137 """Check if tail-call interpreter optimization is available. 

138 

139 Tail-call interpreter is available in Python 3.14+. 

140 """ 

141 return PY314 

142 

143 

144_cached_experimental_enabled: bool | None = None 

145 

146 

147def is_experimental_enabled(*, force_refresh: bool = False) -> bool: 

148 """Check if experimental features are explicitly enabled. 

149 

150 Features are cached after first detection for performance. 

151 

152 Args: 

153 force_refresh: If True, re-detect instead of using cache. 

154 

155 Returns: 

156 True if STACK_ENABLE_EXPERIMENTAL=1 is set. 

157 

158 """ 

159 global _cached_experimental_enabled # noqa: PLW0603 

160 

161 if _cached_experimental_enabled is not None and not force_refresh: 

162 return _cached_experimental_enabled 

163 

164 value = os.environ.get(ENV_ENABLE_EXPERIMENTAL, "").lower() 

165 _cached_experimental_enabled = value in {"1", "true", "yes", "on"} 

166 return _cached_experimental_enabled 

167 

168 

169_cached_optimization_level: int | None = None 

170 

171 

172def get_optimization_level(*, force_refresh: bool = False) -> int: 

173 """Get the configured optimization level. 

174 

175 Features are cached after first detection for performance. 

176 

177 Args: 

178 force_refresh: If True, re-detect instead of using cache. 

179 

180 Returns: 

181 0 = No optimizations 

182 1 = Safe optimizations only (default) 

183 2 = Aggressive optimizations (requires experimental) 

184 

185 """ 

186 global _cached_optimization_level # noqa: PLW0603 

187 

188 if _cached_optimization_level is not None and not force_refresh: 

189 return _cached_optimization_level 

190 

191 try: 

192 level = int(os.environ.get(ENV_OPTIMIZATION_LEVEL, "1")) 

193 _cached_optimization_level = max(0, min(2, level)) # Clamp to 0-2 

194 except ValueError: 

195 _cached_optimization_level = 1 

196 

197 return _cached_optimization_level 

198 

199 

200# ============================================================================= 

201# Feature Data Classes 

202# ============================================================================= 

203 

204 

205@dataclass(frozen=True, slots=True) 

206class PythonFeatures: 

207 """Available Python features based on version and build configuration. 

208 

209 This dataclass is immutable and optimized for performance with slots. 

210 """ 

211 

212 version: tuple[int, int, int] 

213 version_string: str 

214 tier: VersionTier 

215 

216 # Build features 

217 has_jit: bool = False 

218 has_free_threading: bool = False 

219 has_mimalloc: bool = False 

220 has_tail_call_interpreter: bool = False 

221 

222 # Language features by version 

223 has_exception_groups: bool = False # 3.11+ 

224 has_self_type: bool = False # 3.11+ 

225 has_type_params: bool = False # 3.12+ 

226 has_fstring_improvements: bool = False # 3.12+ 

227 has_override_decorator: bool = False # 3.12+ 

228 has_deprecated_decorator: bool = False # 3.13+ 

229 has_deferred_annotations: bool = False # 3.14+ 

230 

231 # Experimental features enabled 

232 experimental_enabled: bool = False 

233 

234 def to_dict(self) -> dict[str, object]: 

235 """Convert to dictionary for serialization.""" 

236 return { 

237 "version": ".".join(map(str, self.version)), 

238 "tier": self.tier.value, 

239 "features": { 

240 "jit": self.has_jit, 

241 "free_threading": self.has_free_threading, 

242 "mimalloc": self.has_mimalloc, 

243 "tail_call_interpreter": self.has_tail_call_interpreter, 

244 }, 

245 "language": { 

246 "exception_groups": self.has_exception_groups, 

247 "self_type": self.has_self_type, 

248 "type_params": self.has_type_params, 

249 "override_decorator": self.has_override_decorator, 

250 "deprecated_decorator": self.has_deprecated_decorator, 

251 "deferred_annotations": self.has_deferred_annotations, 

252 }, 

253 "experimental_enabled": self.experimental_enabled, 

254 } 

255 

256 

257# ============================================================================= 

258# Main Detection Functions 

259# ============================================================================= 

260 

261# Cache the features after first detection 

262_cached_features: PythonFeatures | None = None 

263 

264 

265def get_features(*, force_refresh: bool = False) -> PythonFeatures: 

266 """Detect and return available Python features. 

267 

268 Features are cached after first detection for performance. 

269 

270 Args: 

271 force_refresh: If True, re-detect features instead of using cache. 

272 

273 Returns: 

274 PythonFeatures dataclass with all detected features. 

275 

276 """ 

277 global _cached_features # noqa: PLW0603 - intentional cache pattern 

278 

279 if _cached_features is not None and not force_refresh: 

280 return _cached_features 

281 

282 # Determine version tier 

283 if PY314: 

284 tier = VersionTier.CUTTING_EDGE 

285 elif PY313: 

286 tier = VersionTier.MODERN 

287 elif PY312: 

288 tier = VersionTier.ENHANCED 

289 else: 

290 tier = VersionTier.STABLE 

291 

292 experimental = is_experimental_enabled(force_refresh=force_refresh) 

293 

294 # Build features (only if experimental is enabled for safety) 

295 has_jit = _check_jit_available() if experimental else False 

296 has_free_threading = _check_free_threading_available() if experimental else False 

297 has_mimalloc = _check_mimalloc_available() # Safe to detect 

298 

299 features = PythonFeatures( 

300 version=(PY_VERSION.major, PY_VERSION.minor, PY_VERSION.micro), 

301 version_string=f"{PY_VERSION.major}.{PY_VERSION.minor}.{PY_VERSION.micro}", 

302 tier=tier, 

303 # Build features 

304 has_jit=has_jit, 

305 has_free_threading=has_free_threading, 

306 has_mimalloc=has_mimalloc, 

307 has_tail_call_interpreter=_check_tail_call_interpreter(), 

308 # Language features 

309 has_exception_groups=PY311, 

310 has_self_type=PY311, 

311 has_type_params=PY312, 

312 has_fstring_improvements=PY312, 

313 has_override_decorator=PY312, 

314 has_deprecated_decorator=PY313, 

315 has_deferred_annotations=PY314, 

316 # Experimental 

317 experimental_enabled=experimental, 

318 ) 

319 

320 _cached_features = features 

321 

322 # Log detected features at DEBUG level 

323 logger.debug( 

324 "Python %s detected (tier=%s, experimental=%s): %r", 

325 features.version_string, 

326 tier.value, 

327 experimental, 

328 features, 

329 ) 

330 

331 return features 

332 

333 

334def get_python_info() -> dict[str, object]: 

335 """Get comprehensive Python runtime information. 

336 

337 Returns: 

338 Dictionary with version, platform, and feature information. 

339 

340 """ 

341 import platform # noqa: PLC0415 - lazy import for optional module 

342 

343 features = get_features() 

344 

345 return { 

346 "version": features.version_string, 

347 "version_tuple": features.version, 

348 "tier": features.tier.value, 

349 "implementation": platform.python_implementation(), 

350 "platform": platform.platform(), 

351 "compiler": platform.python_compiler(), 

352 "features": features.to_dict(), 

353 "optimization_level": get_optimization_level(), 

354 }