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

118 statements  

« prev     ^ index     » next       coverage.py v7.14.3, created at 2026-07-06 15:01 +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 platform 

14import sys 

15import sysconfig 

16from dataclasses import dataclass 

17from enum import StrEnum 

18from typing import Final 

19 

20__all__ = [ 

21 "PY311", 

22 "PY312", 

23 "PY313", 

24 "PY314", 

25 "PY_VERSION", 

26 "PythonFeatures", 

27 "VersionTier", 

28 "get_features", 

29 "get_python_info", 

30 "is_experimental_enabled", 

31] 

32 

33logger = logging.getLogger(__name__) 

34 

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

36# Version Constants 

37# ============================================================================= 

38 

39PY_VERSION: Final = sys.version_info 

40"""Current Python version tuple.""" 

41 

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

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

44 

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

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

47 

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

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

50 

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

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

53 

54 

55class VersionTier(StrEnum): 

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

57 

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

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

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

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

62 

63 

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

65# Environment Variables for Experimental Features 

66# ============================================================================= 

67 

68ENV_ENABLE_EXPERIMENTAL = "STACK_ENABLE_EXPERIMENTAL" 

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

70 

71ENV_OPTIMIZATION_LEVEL = "STACK_OPTIMIZATION_LEVEL" 

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

73 

74 

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

76# Feature Detection Functions 

77# ============================================================================= 

78 

79 

80def _check_jit_available() -> bool: 

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

82 

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

84 """ 

85 if not PY313: 

86 return False 

87 

88 try: 

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

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

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

92 except (AttributeError, TypeError): 

93 return False 

94 

95 

96def _check_free_threading_available() -> bool: 

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

98 

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

100 """ 

101 if not PY313: 

102 return False 

103 

104 try: 

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

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

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

108 return bool(sys.flags.nogil) 

109 

110 # Alternative check for 3.13+ 

111 

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

113 except (AttributeError, TypeError): 

114 return False 

115 else: 

116 return "--disable-gil" in config_args 

117 

118 

119def _check_mimalloc_available() -> bool: 

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

121 

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

123 """ 

124 if not PY313: 

125 return False 

126 

127 try: 

128 # Check if built with mimalloc 

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

130 return "mimalloc" in config_args.lower() 

131 except (AttributeError, TypeError): 

132 return False 

133 

134 

135def _check_tail_call_interpreter() -> bool: 

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

137 

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

139 """ 

140 return PY314 

141 

142 

143_cached_experimental_enabled: bool | None = None 

144 

145 

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

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

148 

149 Features are cached after first detection for performance. 

150 

151 Args: 

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

153 

154 Returns: 

155 True if STACK_ENABLE_EXPERIMENTAL=1 is set. 

156 

157 """ 

158 global _cached_experimental_enabled # noqa: PLW0603 

159 

160 if _cached_experimental_enabled is not None and not force_refresh: 

161 return _cached_experimental_enabled 

162 

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

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

165 return _cached_experimental_enabled 

166 

167 

168_cached_optimization_level: int | None = None 

169 

170 

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

172 """Get the configured optimization level. 

173 

174 Features are cached after first detection for performance. 

175 

176 Args: 

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

178 

179 Returns: 

180 0 = No optimizations 

181 1 = Safe optimizations only (default) 

182 2 = Aggressive optimizations (requires experimental) 

183 

184 """ 

185 global _cached_optimization_level # noqa: PLW0603 

186 

187 if _cached_optimization_level is not None and not force_refresh: 

188 return _cached_optimization_level 

189 

190 try: 

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

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

193 except ValueError: 

194 _cached_optimization_level = 1 

195 

196 return _cached_optimization_level 

197 

198 

199# ============================================================================= 

200# Feature Data Classes 

201# ============================================================================= 

202 

203 

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

205class PythonFeatures: 

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

207 

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

209 """ 

210 

211 version: tuple[int, int, int] 

212 version_string: str 

213 tier: VersionTier 

214 

215 # Build features 

216 has_jit: bool = False 

217 has_free_threading: bool = False 

218 has_mimalloc: bool = False 

219 has_tail_call_interpreter: bool = False 

220 

221 # Language features by version 

222 has_exception_groups: bool = False # 3.11+ 

223 has_self_type: bool = False # 3.11+ 

224 has_type_params: bool = False # 3.12+ 

225 has_override_decorator: bool = False # 3.12+ 

226 has_deprecated_decorator: bool = False # 3.13+ 

227 has_deferred_annotations: bool = False # 3.14+ 

228 

229 # Experimental features enabled 

230 experimental_enabled: bool = False 

231 

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

233 """Convert to dictionary for serialization.""" 

234 return { 

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

236 "tier": self.tier.value, 

237 "features": { 

238 "jit": self.has_jit, 

239 "free_threading": self.has_free_threading, 

240 "mimalloc": self.has_mimalloc, 

241 "tail_call_interpreter": self.has_tail_call_interpreter, 

242 }, 

243 "language": { 

244 "exception_groups": self.has_exception_groups, 

245 "self_type": self.has_self_type, 

246 "type_params": self.has_type_params, 

247 "override_decorator": self.has_override_decorator, 

248 "deprecated_decorator": self.has_deprecated_decorator, 

249 "deferred_annotations": self.has_deferred_annotations, 

250 }, 

251 "experimental_enabled": self.experimental_enabled, 

252 } 

253 

254 

255def _get_version_tier() -> VersionTier: 

256 """Determine the version tier based on Python version.""" 

257 if PY314: 

258 return VersionTier.CUTTING_EDGE 

259 if PY313: 

260 return VersionTier.MODERN 

261 if PY312: 

262 return VersionTier.ENHANCED 

263 return VersionTier.STABLE 

264 

265 

266def _get_build_features(experimental: bool) -> dict[str, bool]: 

267 """Determine build features based on experimental flag.""" 

268 return { 

269 "has_jit": _check_jit_available() if experimental else False, 

270 "has_free_threading": _check_free_threading_available() 

271 if experimental 

272 else False, 

273 "has_mimalloc": _check_mimalloc_available(), 

274 "has_tail_call_interpreter": _check_tail_call_interpreter(), 

275 } 

276 

277 

278def _get_language_features() -> dict[str, bool]: 

279 """Determine language features based on Python version.""" 

280 return { 

281 "has_exception_groups": PY311, 

282 "has_self_type": PY311, 

283 "has_type_params": PY312, 

284 "has_override_decorator": PY312, 

285 "has_deprecated_decorator": PY313, 

286 "has_deferred_annotations": PY314, 

287 } 

288 

289 

290# ============================================================================= 

291# Main Detection Functions 

292# ============================================================================= 

293 

294# Cache the features after first detection 

295_cached_features: PythonFeatures | None = None 

296 

297 

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

299 """Detect and return available Python features. 

300 

301 Features are cached after first detection for performance. 

302 

303 Args: 

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

305 

306 Returns: 

307 PythonFeatures dataclass with all detected features. 

308 

309 """ 

310 global _cached_features # noqa: PLW0603 - intentional cache pattern 

311 

312 if _cached_features is not None and not force_refresh: 

313 return _cached_features 

314 

315 tier = _get_version_tier() 

316 experimental = is_experimental_enabled(force_refresh=force_refresh) 

317 build_feats = _get_build_features(experimental) 

318 lang_feats = _get_language_features() 

319 

320 features = PythonFeatures( 

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

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

323 tier=tier, 

324 **build_feats, 

325 **lang_feats, 

326 experimental_enabled=experimental, 

327 ) 

328 

329 _cached_features = features 

330 

331 # Log detected features at DEBUG level 

332 logger.debug( 

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

334 features.version_string, 

335 tier.value, 

336 experimental, 

337 features, 

338 ) 

339 

340 return features 

341 

342 

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

344 """Get comprehensive Python runtime information. 

345 

346 Returns: 

347 Dictionary with version, platform, and feature information. 

348 

349 """ 

350 features = get_features() 

351 

352 return { 

353 "version": features.version_string, 

354 "version_tuple": features.version, 

355 "tier": features.tier.value, 

356 "implementation": platform.python_implementation(), 

357 "platform": platform.platform(), 

358 "compiler": platform.python_compiler(), 

359 "features": features.to_dict(), 

360 "optimization_level": get_optimization_level(), 

361 }